UserHero Docs
API Reference

Webhook Payloads

Webhook event formats and payload reference

Webhook Payloads

Reference documentation for webhook event payloads.

Event Types

EventDescription
feedback.createdNew feedback submitted
feedback.updatedFeedback status changed

Common Structure

All webhook payloads share this structure:

{
  "event": "string",
  "timestamp": "ISO 8601 datetime",
  "data": { ... }
}

feedback.created

Sent when new feedback is submitted.

{
  "event": "feedback.created",
  "timestamp": "2026-01-07T15:30:00.000Z",
  "data": {
    "id": "fb_abc123xyz",
    "projectId": "proj_xyz789",
    "widgetId": "w_default",
    "widgetName": "Main Feedback Widget",
    "template": "floating",
    
    "message": "The export button doesn't work when I select more than 100 items. I've tried multiple times with different browsers.",
    "category": "bug",
    "rating": 2,
    "email": "user@example.com",
    
    "status": "new",
    
    "screenshotUrl": "https://storage.userhero.co/screenshots/fb_abc123xyz.png",
    
    "context": {
      "pageUrl": "/dashboard/reports",
      "fullUrl": "https://app.example.com/dashboard/reports?view=list",
      "referrer": "https://app.example.com/dashboard",
      "browser": "Chrome",
      "browserVersion": "120.0.6099.129",
      "os": "macOS",
      "osVersion": "14.2",
      "deviceType": "desktop",
      "viewport": {
        "width": 1920,
        "height": 1080
      },
      "timezone": "America/New_York",
      "locale": "en-US",
      "country": "US"
    },
    
    "metadata": {
      "userId": "user_123",
      "plan": "pro",
      "company": "Acme Inc",
      "accountId": "acc_456",
      "role": "admin"
    },
    
    "createdAt": "2026-01-07T15:30:00.000Z"
  }
}

Fields

FieldTypeDescription
idstringUnique feedback ID
projectIdstringProject ID
widgetIdstringWidget ID
widgetNamestringWidget display name
templatestringWidget template (floating, bug-report, rating)
messagestringUser's feedback text
categorystring | nullSelected category
ratingnumber | nullRating score (1-5 or 1-10)
emailstring | nullUser's email if provided
statusstringFeedback status (new, in-progress, resolved, archived)
screenshotUrlstring | nullScreenshot URL if captured
contextobjectAutomatically collected context
metadataobject | nullCustom metadata from setMetadata()
createdAtstringISO 8601 timestamp

Context Object

FieldTypeDescription
pageUrlstringPage path
fullUrlstringComplete URL (if enabled)
referrerstringPrevious page URL
browserstringBrowser name
browserVersionstringBrowser version
osstringOperating system
osVersionstringOS version
deviceTypestringdesktop, mobile, tablet
viewport.widthnumberScreen width
viewport.heightnumberScreen height
timezonestringIANA timezone
localestringLanguage locale
countrystring2-letter country code

feedback.updated

Sent when feedback status is changed.

{
  "event": "feedback.updated",
  "timestamp": "2026-01-07T16:45:00.000Z",
  "data": {
    "id": "fb_abc123xyz",
    "projectId": "proj_xyz789",
    "previousStatus": "new",
    "newStatus": "in-progress",
    "updatedBy": {
      "id": "user_team123",
      "email": "team@example.com",
      "name": "John Smith"
    },
    "updatedAt": "2026-01-07T16:45:00.000Z"
  }
}

Signature Verification

Webhooks include a signature header for verification:

X-UserHero-Signature: sha256=abc123def456...

Verification Code

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expectedSignature = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Usage
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-userhero-signature'];
  
  if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Process webhook
  const { event, data } = req.body;
  // ...
  
  res.status(200).json({ received: true });
});

Response Requirements

Your endpoint should:

  • Return 2xx status code
  • Respond within 30 seconds
  • Return quickly (process async if needed)

Retry Policy

Failed deliveries are retried with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
624 hours

After 6 failures, webhook is marked as failing.

Testing

Use the test webhook feature in your dashboard:

  1. Go to IntegrationsWebhooks
  2. Click "Send Test Event"
  3. Select event type
  4. Verify your endpoint receives it

Best Practices

  1. Verify signatures: Always verify the signature in production
  2. Respond fast: Return 200 immediately, process async
  3. Idempotency: Handle duplicate events gracefully
  4. Logging: Log webhook payloads for debugging
  5. Error handling: Return 5xx for retryable errors, 4xx for permanent failures

Next Steps

On this page