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 ID. System values: new, in-progress, reopened, resolved, archived. Custom status IDs are also accepted. |
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 |
ticketNumber | number | null | Human-facing ticket number, unique per workspace (e.g. 245). Displayed as #245. |
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 ID |
statusCategory | string | Behavior category: open, pending, resolved, or closed |
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 |
customMetadata | object | null | Arbitrary key/value data attached to the feedback, including any customFields values (see Custom Fields) |
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 | Arbitrary key/value object attached to the feedback. Accepts any JSON keys at the top level. Also supports a reserved customFields sub-key for validated custom field values (see Custom Fields). |
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.
Get Feedback by Ticket Number
GET /api/v1/feedback/number/{ticketNumber}?workspaceId={id}Look up a feedback item by its workspace-scoped ticket number. Accepts a plain number (245) or a hash-prefixed string (%23245).
Query parameters:
| Parameter | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace ID |
curl https://userhero.co/api/v1/feedback/number/245?workspaceId=ws_123 \
-H "Authorization: Bearer sk_live_your_key"Response shape matches Get Feedback.
Update Feedback
PATCH /api/v1/feedback/{id}Body parameters:
| Parameter | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace ID |
status | No | Status ID to set. System values: new, in-progress, reopened, resolved, archived. Custom status IDs (from your project settings) are also accepted. |
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}Custom Fields
For a complete guide to creating and configuring custom fields, see Custom Fields.
Custom fields let you attach structured data to feedback — either from the API or via the feedback widget. Fields are defined per-project in the dashboard under Project Settings > Custom Fields.
customMetadata structure
customMetadata is an opaque key/value object that supports two types of data:
1. Arbitrary key/value pairs — any JSON key/value you want to store alongside the feedback (e.g. userId, plan, company). Top-level keys matching common name, email, or phone patterns are automatically promoted to the top-level userName, userEmail, and userPhone fields.
2. Validated custom fields — a reserved customFields sub-key that maps to the project's defined custom fields (df_* and cf_*). These values are validated against the field definitions and displayed as structured fields in the dashboard.
{
"customMetadata": {
"userId": "usr_123",
"plan": "enterprise",
"company": "Acme Inc",
"customFields": {
"df_name": "Alice",
"df_email": "alice@example.com",
"cf_plan_abc12": "Enterprise",
"cf_account_xyz": "acc_99887"
}
}
}Both can be used independently or together.
Field IDs
| Prefix | Description |
|---|---|
df_name | Submitter name |
df_email | Submitter email |
df_phone | Submitter phone |
df_company | Submitter company |
cf_xxxxx | Project-defined custom field |
Default fields (df_*) are available on all plans. Project-defined custom fields (cf_*) require a Pro or Pro Max plan to create, but any plan can submit values for existing fields.
To find your cf_* field IDs, open the project in the dashboard — each custom field shows its ID below the label in Project Settings > Custom Fields. You can also retrieve them programmatically via Get Project: the response includes a customFieldDefinitions array with each field's id, label, and type.
Submitting custom field values
Include a customFields sub-key inside customMetadata. Values are validated against the field definitions configured in the project.
{
"workspaceId": "ws_123",
"projectId": "proj_456",
"message": "Checkout is broken",
"customMetadata": {
"userId": "usr_123",
"plan": "enterprise",
"customFields": {
"df_name": "Alice",
"df_email": "alice@example.com",
"cf_plan_abc12": "Enterprise",
"cf_account_xyz": "acc_99887"
}
}
}Reading custom field values
The full customMetadata object is returned on all feedback GET responses (list and single item):
{
"id": "fb_abc123",
"message": "Checkout is broken",
"customMetadata": {
"userId": "usr_123",
"plan": "enterprise",
"customFields": {
"df_name": "Alice",
"df_email": "alice@example.com",
"cf_plan_abc12": "Enterprise",
"cf_account_xyz": "acc_99887"
}
}
}Default fields (df_name, df_email, df_phone, df_company) are also promoted to the top-level feedback fields (userName, userEmail, userPhone) automatically.
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}Response fields:
| Field | Type | Description |
|---|---|---|
id | string | Project ID |
workspaceId | string | Workspace ID |
name | string | Project name |
type | string | Project type |
domain | string | null | Associated domain |
bundleId | string | null | iOS bundle ID |
packageName | string | null | Android package name |
publicKey | string | Public key for the widget SDK |
privacySettings | object | Privacy configuration |
publicPortal | object | null | Public portal configuration |
customFieldDefinitions | object[] | Project-defined custom fields. Each entry: { id, type, label, required, placeholder?, options?, validation?, isDefault?, visibilityCondition?, optionsByParent? }. options is an array of strings for select fields. validation is an object with optional min, max, minLength, maxLength. visibilityCondition (optional) is { fieldId: string, value: string } — present when the field only shows if another select field equals a given value. optionsByParent (optional) is { parentFieldId: string, map: Record<string, string[]> } — present on select fields that filter their options based on a parent field's value. See Dependent Fields. |
disabledDefaultFields | string[] | IDs of default fields disabled for this project (e.g. ["df_phone"]) |
createdAt | string | ISO 8601 timestamp |
updatedAt | string | ISO 8601 timestamp |
{
"data": {
"id": "proj_456",
"workspaceId": "ws_123",
"name": "My App",
"domain": "app.example.com",
"publicKey": "pk_live_abc123",
"customFieldDefinitions": [
{ "id": "cf_plan_abc12", "label": "Plan", "type": "select", "required": false, "options": ["Free", "Starter", "Pro"] },
{ "id": "cf_score_xyz", "label": "NPS Score", "type": "number", "required": false, "validation": { "min": 0, "max": 10 } },
{ "id": "cf_notes_001", "label": "Notes", "type": "textarea", "required": false, "placeholder": "Any additional context" }
],
"disabledDefaultFields": [],
"createdAt": "2024-08-12T09:21:04.000Z",
"updatedAt": "2025-01-04T14:02:11.000Z"
}
}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
List Workspaces
GET /api/v1/workspacesReturns every workspace the API key has access to. A workspace is included only if:
- The key's user is an owner or admin of the workspace.
- The workspace is on a plan that supports REST API access (Pro).
- The workspace has not disabled API access in Settings > Integrations.
No query parameters. Required scope: workspaces:read.
Example response:
{
"data": [
{
"id": "ws_123",
"name": "Acme Inc",
"plan": "pro",
"billingInterval": "yearly",
"subscriptionStatus": "active",
"apiAccessEnabled": true,
"memberCount": 6,
"role": "owner",
"createdAt": "2024-08-12T09:21:04.000Z",
"updatedAt": "2025-01-04T14:02:11.000Z"
}
]
}Use this endpoint to discover the workspace IDs you need for the other endpoints that take workspaceId.
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 Or Lookup Customers
GET /api/v1/customers?workspaceId={id}This endpoint has two modes:
- List mode: pagination/filter/search when
emailandphoneare not provided - Lookup mode: exact customer lookup when
emailand/orphoneis provided
Query parameters:
| Parameter | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace ID |
email | No | Exact match lookup by email address (normalized lowercase) |
phone | No | Exact match lookup by phone number |
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) |
When email or phone is provided, the endpoint switches to lookup mode and returns a single customer object:
emailonly: returns the exact matching customer by emailphoneonly: returns the exact matching customer by phoneemail+phone: both must resolve to the same customer
Lookup mode errors:
404 Not Found: no customer matches the provided identifier409 Conflict: identifiers are ambiguous (for example, email and phone map to different customers)
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
}
}Automations
The automations API lets you list, inspect, and execute automations programmatically. Automations are project-scoped — you always filter by projectId. Read operations require the automations:read scope; write operations and running macros require automations:write.
List automations
GET /api/v1/automationsQuery parameters:
| Parameter | Required | Description |
|---|---|---|
projectId | Yes | Project ID |
scope | No | Filter by scope: feedback.created, feedback.updated, time.scheduled, manual |
enabled | No | Filter by enabled state: true or false |
includeSystem | No | Include SLA-generated automations — default false |
limit | No | Results per page — max 100, default 50 |
cursor | No | Pagination cursor (last automation ID from previous response) |
Response:
{
"automations": [
{
"id": "jg9bTVq5gSvjVm0L55cS",
"name": "Auto-assign billing",
"scope": "feedback.created",
"enabled": true,
"priority": 0,
"systemGenerated": false,
"conditions": { "combinator": "and", "rules": [] },
"actions": [{ "type": "assign_agent", "params": { "userId": "user_abc" } }],
"runStats": { "matched7d": 42, "errored7d": 0 },
"lastRunAt": "2026-05-30T10:00:00.000Z",
"createdAt": "2026-04-01T00:00:00.000Z",
"updatedAt": "2026-05-01T00:00:00.000Z"
}
],
"nextCursor": "def456"
}Pass nextCursor as cursor to fetch the next page.
Get an automation
GET /api/v1/automations/{id}Returns full automation details including conditions, actions, and run statistics.
Response fields:
| Field | Type | Description |
|---|---|---|
id | string | Automation ID |
name | string | Display name |
scope | string | When the automation runs: feedback.created, feedback.updated, time.scheduled, or manual |
enabled | boolean | Whether the automation is active |
priority | number | Execution order (lower runs first) |
systemGenerated | boolean | true for SLA-generated automations |
conditions | object | Condition tree (combinator + rules) |
actions | array | Ordered list of action steps |
runStats.matched7d | number | Runs that matched in the last 7 days |
runStats.errored7d | number | Runs that errored in the last 7 days |
lastRunAt | string | null | ISO timestamp of last execution |
Run a macro
POST /api/v1/automations/{id}/runExecute a macro automation against a specific feedback item. The automation must have scope: "manual".
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
feedbackId | string | Yes | The feedback item to run the macro on |
Response:
{
"runId": "run_xyz789",
"outcome": "matched",
"actionsExecuted": [
{ "type": "set_status", "success": true },
{ "type": "add_tag", "success": true }
]
}| Field | Description |
|---|---|
outcome | matched — conditions passed and actions ran; no-match — conditions did not pass; error — one or more actions failed |
actionsExecuted | Per-action result with type, success, and optional error message |
List automation runs
GET /api/v1/automations/{id}/runsReturns the last 50 execution runs for an automation, most recent first.
Response:
{
"runs": [
{
"id": "run_xyz789",
"outcome": "matched",
"runAt": "2026-05-30T10:00:00.000Z",
"feedbackId": "feedback_abc123",
"actionsExecuted": [
{ "type": "set_status", "success": true }
],
"actor": { "userId": "user_abc", "name": "Alice", "system": false }
}
]
}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"}'