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 ID. System values: new, in-progress, reopened, resolved, archived. Custom status IDs are also accepted.
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
ticketNumbernumber | nullHuman-facing ticket number, unique per workspace (e.g. 245). Displayed as #245.
workspaceIdstringWorkspace ID
projectIdstringProject ID
widgetIdstringWidget ID or "api"
messagestringFeedback text
categorystring | nullFeedback category
ratingnumber | nullRating value
statusstringCurrent status ID
statusCategorystringBehavior category: open, pending, resolved, or closed
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
customMetadataobject | nullArbitrary key/value data attached to the feedback, including any customFields values (see Custom Fields)
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)
customMetadataNoArbitrary 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).
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.

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:

ParameterRequiredDescription
workspaceIdYesWorkspace 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:

ParameterRequiredDescription
workspaceIdYesWorkspace ID
statusNoStatus ID to set. System values: new, in-progress, reopened, resolved, archived. Custom status IDs (from your project settings) are also accepted.
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}

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

PrefixDescription
df_nameSubmitter name
df_emailSubmitter email
df_phoneSubmitter phone
df_companySubmitter company
cf_xxxxxProject-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}/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}

Response fields:

FieldTypeDescription
idstringProject ID
workspaceIdstringWorkspace ID
namestringProject name
typestringProject type
domainstring | nullAssociated domain
bundleIdstring | nulliOS bundle ID
packageNamestring | nullAndroid package name
publicKeystringPublic key for the widget SDK
privacySettingsobjectPrivacy configuration
publicPortalobject | nullPublic portal configuration
customFieldDefinitionsobject[]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.
disabledDefaultFieldsstring[]IDs of default fields disabled for this project (e.g. ["df_phone"])
createdAtstringISO 8601 timestamp
updatedAtstringISO 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:

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

List Workspaces

GET /api/v1/workspaces

Returns 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}/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 Or Lookup Customers

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

This endpoint has two modes:

  • List mode: pagination/filter/search when email and phone are not provided
  • Lookup mode: exact customer lookup when email and/or phone is provided

Query parameters:

ParameterRequiredDescription
workspaceIdYesWorkspace ID
emailNoExact match lookup by email address (normalized lowercase)
phoneNoExact match lookup by phone number
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)

When email or phone is provided, the endpoint switches to lookup mode and returns a single customer object:

  • email only: returns the exact matching customer by email
  • phone only: returns the exact matching customer by phone
  • email + phone: both must resolve to the same customer

Lookup mode errors:

  • 404 Not Found: no customer matches the provided identifier
  • 409 Conflict: identifiers are ambiguous (for example, email and phone map to different customers)

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
  }
}

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

Query parameters:

ParameterRequiredDescription
projectIdYesProject ID
scopeNoFilter by scope: feedback.created, feedback.updated, time.scheduled, manual
enabledNoFilter by enabled state: true or false
includeSystemNoInclude SLA-generated automations — default false
limitNoResults per page — max 100, default 50
cursorNoPagination 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:

FieldTypeDescription
idstringAutomation ID
namestringDisplay name
scopestringWhen the automation runs: feedback.created, feedback.updated, time.scheduled, or manual
enabledbooleanWhether the automation is active
prioritynumberExecution order (lower runs first)
systemGeneratedbooleantrue for SLA-generated automations
conditionsobjectCondition tree (combinator + rules)
actionsarrayOrdered list of action steps
runStats.matched7dnumberRuns that matched in the last 7 days
runStats.errored7dnumberRuns that errored in the last 7 days
lastRunAtstring | nullISO timestamp of last execution

Run a macro

POST /api/v1/automations/{id}/run

Execute a macro automation against a specific feedback item. The automation must have scope: "manual".

Request body:

FieldTypeRequiredDescription
feedbackIdstringYesThe feedback item to run the macro on

Response:

{
  "runId": "run_xyz789",
  "outcome": "matched",
  "actionsExecuted": [
    { "type": "set_status", "success": true },
    { "type": "add_tag", "success": true }
  ]
}
FieldDescription
outcomematched — conditions passed and actions ran; no-match — conditions did not pass; error — one or more actions failed
actionsExecutedPer-action result with type, success, and optional error message

List automation runs

GET /api/v1/automations/{id}/runs

Returns 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"}'

On this page