Production-ready examples for ingesting feedback from various sources.

Field Reference

When creating fields, use these valid categories and types: Categories: content, created_at, identifier, identity, metadata Types: boolean, date, datetime, float, integer, json, string

Support Ticket Integration

Sync your customer support tickets to analyze alongside calls and product data.
// Zendesk webhook handler (Node.js/Express)
const express = require('express');
const axios = require('axios');

app.post('/webhooks/zendesk', async (req, res) => {
  const ticket = req.body;
  
  try {
    // First, ensure the customer exists
    await axios.put('https://api.buildbetter.app/v3/rest/people', {
      email: ticket.requester.email,
      first_name: ticket.requester.first_name,
      last_name: ticket.requester.last_name,
      company: {
        name: ticket.organization?.name,
        domain: ticket.requester.email.split('@')[1]
      }
    }, {
      headers: {
        'X-BuildBetter-Api-Key': 'YOUR_ORGANIZATION_API_KEY'
      }
    });
    
    // Then ingest the ticket
    await axios.post(
      'https://api.buildbetter.app/v3/rest/feedback-sources/123/records',
      {
        person_id: ticket.requester.id,
        display_ts: ticket.created_at,
        external_id: `zendesk-${ticket.id}`,
        fields: [
          {
            category: 'content',
            type: 'string',
            name: 'subject',
            value: ticket.subject
          },
          {
            category: 'content',
            type: 'string',
            name: 'description',
            value: ticket.description
          },
          {
            category: 'metadata',
            type: 'string',
            name: 'priority',
            value: ticket.priority
          },
          {
            category: 'metadata',
            type: 'string',
            name: 'status',
            value: ticket.status
          },
          {
            category: 'metadata',
            type: 'string',
            name: 'tags',
            value: ticket.tags.join(', ')
          }
        ]
      },
      {
        headers: {
          'X-BuildBetter-Api-Key': 'ORGANIZATION_API_KEY'
        }
      }
    );
    
    res.status(200).send('Ticket ingested successfully');
  } catch (error) {
    console.error('Failed to ingest ticket:', error);
    res.status(500).send('Error processing ticket');
  }
});

NPS Survey Integration

Capture Net Promoter Score feedback to track customer satisfaction trends.
// Delighted NPS webhook handler
const processDelightedSurvey = async (surveyData) => {
  const { person, score, comment, created_at } = surveyData;
  
  // Calculate NPS category
  let category;
  if (score >= 9) category = 'promoter';
  else if (score >= 7) category = 'passive';
  else category = 'detractor';
  
  const feedbackPayload = {
    display_ts: new Date(created_at * 1000).toISOString(),
    external_id: `delighted-${surveyData.id}`,
    fields: [
      {
        category: 'content',
        type: 'integer',
        name: 'nps_score',
        value: score
      },
      {
        category: 'content',
        type: 'string',
        name: 'feedback',
        value: comment || ''
      },
      {
        category: 'metadata',
        type: 'string',
        name: 'nps_category',
        value: category
      },
      {
        category: 'metadata',
        type: 'string',
        name: 'survey_platform',
        value: 'delighted'
      },
      {
        category: 'metadata',
        type: 'json',
        name: 'person_properties',
        value: surveyData.person_properties || {}
      }
    ]
  };
  
  // If person has email, ensure they exist first
  if (person.email) {
    await ensurePersonExists({
      email: person.email,
      first_name: person.name?.split(' ')[0],
      last_name: person.name?.split(' ').slice(1).join(' ')
    });
    
    feedbackPayload.person_id = await getPersonId(person.email);
  }
  
  return await ingestFeedback(feedbackPayload);
};

Slack Feedback Capture

Monitor Slack channels for product feedback and feature requests.
// Slack bot that captures messages with specific emoji reactions
const { App } = require('@slack/bolt');
const axios = require('axios');

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET
});

// Listen for feedback emoji reactions (e.g., :feedback:)
app.event('reaction_added', async ({ event, client }) => {
  if (event.reaction !== 'feedback') return;
  
  try {
    // Get the message that was reacted to
    const message = await client.conversations.history({
      channel: event.item.channel,
      latest: event.item.ts,
      limit: 1,
      inclusive: true
    });
    
    const messageData = message.messages[0];
    
    // Get user info
    const user = await client.users.info({
      user: messageData.user
    });
    
    // Get channel info
    const channel = await client.conversations.info({
      channel: event.item.channel
    });
    
    // Ingest to BuildBetter
    await axios.post(
      'https://api.buildbetter.app/v3/rest/feedback-sources/125/records',
      {
        display_ts: new Date(parseFloat(messageData.ts) * 1000).toISOString(),
        external_id: `slack-${event.item.channel}-${event.item.ts}`,
        fields: [
          {
            category: 'content',
            type: 'string',
            name: 'message',
            value: messageData.text
          },
          {
            category: 'metadata',
            type: 'string',
            name: 'channel',
            value: channel.channel.name
          },
          {
            category: 'metadata',
            type: 'string',
            name: 'user',
            value: user.user.real_name
          },
          {
            category: 'metadata',
            type: 'string',
            name: 'user_email',
            value: user.user.profile.email
          },
          {
            category: 'metadata',
            type: 'string',
            name: 'thread_ts',
            value: messageData.thread_ts || messageData.ts
          }
        ]
      },
      {
        headers: {
          'X-BuildBetter-Api-Key': process.env.BUILDBETTER_API_KEY
        }
      }
    );
    
    // Add confirmation reaction
    await client.reactions.add({
      channel: event.item.channel,
      timestamp: event.item.ts,
      name: 'white_check_mark'
    });
    
  } catch (error) {
    console.error('Error processing feedback:', error);
  }
});

Product Analytics Events

Ingest product usage events and user behavior data.
// Segment webhook handler for product events
interface SegmentEvent {
  type: 'track' | 'identify' | 'page' | 'screen';
  userId?: string;
  anonymousId: string;
  event?: string;
  properties?: Record<string, any>;
  traits?: Record<string, any>;
  timestamp: string;
}

class SegmentToBuildbetter {
  private apiKey: string;
  private feedbackSourceId: number;
  
  constructor(apiKey: string, feedbackSourceId: number) {
    this.apiKey = apiKey;
    this.feedbackSourceId = feedbackSourceId;
  }
  
  async processEvent(event: SegmentEvent): Promise<void> {
    // Only process specific events that are feedback-related
    const feedbackEvents = [
      'Feedback Submitted',
      'Feature Requested',
      'Bug Reported',
      'Rating Provided',
      'Survey Completed'
    ];
    
    if (event.type !== 'track' || !feedbackEvents.includes(event.event!)) {
      return;
    }
    
    const fields = [];
    
    // Add the main event as content
    fields.push({
      category: 'content',
      type: 'string',
      name: 'event',
      value: event.event
    });
    
    // Add relevant properties
    if (event.properties?.feedback) {
      fields.push({
        category: 'content',
        type: 'string',
        name: 'feedback',
        value: event.properties.feedback
      });
    }
    
    if (event.properties?.rating) {
      fields.push({
        category: 'content',
        type: 'integer',
        name: 'rating',
        value: event.properties.rating
      });
    }
    
    // Add metadata
    fields.push(
      {
        category: 'metadata',
        type: 'string',
        name: 'source',
        value: 'segment'
      },
      {
        category: 'metadata',
        type: 'string',
        name: 'user_id',
        value: event.userId || event.anonymousId
      }
    );
    
    // Add any custom properties as metadata
    Object.entries(event.properties || {}).forEach(([key, value]) => {
      if (!['feedback', 'rating'].includes(key)) {
        fields.push({
          category: 'metadata',
          type: 'string',
          name: key,
          value: String(value)
        });
      }
    });
    
    await fetch(
      `https://api.buildbetter.app/v3/rest/feedback-sources/${this.feedbackSourceId}/records`,
      {
        method: 'POST',
        headers: {
          'X-BuildBetter-Api-Key': this.apiKey,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          display_ts: event.timestamp,
          external_id: `segment-${event.type}-${Date.now()}`,
          fields
        })
      }
    );
  }
}

Email Feedback Processing

Process feedback received via email or contact forms.
// Email parser for feedback (using SendGrid Inbound Parse)
const sgMail = require('@sendgrid/mail');
const multer = require('multer');
const upload = multer();

app.post('/email/feedback', upload.none(), async (req, res) => {
  const { from, subject, text, html, envelope } = req.body;
  
  // Parse sender email
  const fromEmail = from.match(/<(.+)>/)?.[1] || from;
  const fromName = from.match(/^([^<]+)/)?.[1]?.trim() || '';
  
  // Parse email metadata
  const envelopeData = JSON.parse(envelope);
  
  // Extract feedback category from subject or email address
  let category = 'general';
  if (subject.toLowerCase().includes('bug')) category = 'bug';
  else if (subject.toLowerCase().includes('feature')) category = 'feature_request';
  else if (envelopeData.to.includes('support@')) category = 'support';
  
  // Clean up email text (remove signatures, quotes, etc.)
  const cleanText = text
    .split(/^>|^On .+ wrote:|^-{2,}|^_{2,}/m)[0]
    .trim();
  
  // Ensure person exists
  const personData = {
    email: fromEmail,
    first_name: fromName.split(' ')[0],
    last_name: fromName.split(' ').slice(1).join(' ')
  };
  
  const personResponse = await axios.put(
    'https://api.buildbetter.app/v3/rest/people',
    personData,
    {
      headers: { 'X-BuildBetter-Api-Key': 'ORGANIZATION_API_KEY' }
    }
  );
  
  // Ingest feedback
  const feedbackData = {
    person_id: personResponse.data.id,
    display_ts: new Date().toISOString(),
    external_id: `email-${Date.now()}-${fromEmail}`,
    fields: [
      {
        category: 'content',
        type: 'string',
        name: 'subject',
        value: subject
      },
      {
        category: 'content',
        type: 'string',
        name: 'message',
        value: cleanText
      },
      {
        category: 'metadata',
        type: 'string',
        name: 'category',
        value: category
      },
      {
        category: 'metadata',
        type: 'string',
        name: 'source',
        value: 'email'
      },
      {
        category: 'metadata',
        type: 'string',
        name: 'to_address',
        value: envelopeData.to[0]
      }
    ]
  };
  
  await axios.post(
    'https://api.buildbetter.app/v3/rest/feedback-sources/126/records',
    feedbackData,
    {
      headers: { 'X-BuildBetter-Api-Key': 'ORGANIZATION_API_KEY' }
    }
  );
  
  // Send auto-reply
  sgMail.setApiKey(process.env.SENDGRID_API_KEY);
  await sgMail.send({
    to: fromEmail,
    from: 'feedback@yourcompany.com',
    subject: 'Re: ' + subject,
    text: `Thank you for your feedback! We've received your message and will review it shortly.
    
    Your feedback helps us improve our product.
    
    Reference ID: ${feedbackData.external_id}`
  });
  
  res.status(200).send('OK');
});

Batch Import from CSV

Import historical feedback data from CSV files.
import csv
import requests
from datetime import datetime
import time

class CSVFeedbackImporter:
    def __init__(self, api_key, feedback_source_id):
        self.api_key = api_key
        self.feedback_source_id = feedback_source_id
        self.base_url = "https://api.buildbetter.app/v3/rest"
        
    def import_csv(self, csv_file_path, mapping):
        """
        Import feedback from CSV file
        
        mapping = {
            'email': 'customer_email',  # CSV column -> field name
            'feedback': 'comment',
            'score': 'rating',
            'date': 'created_at'
        }
        """
        
        with open(csv_file_path, 'r') as file:
            reader = csv.DictReader(file)
            
            success_count = 0
            error_count = 0
            
            for row_num, row in enumerate(reader, start=2):
                try:
                    # Map CSV columns to our structure
                    email = row.get(mapping.get('email'))
                    
                    # Create person if email exists
                    person_id = None
                    if email:
                        person_resp = requests.put(
                            f"{self.base_url}/people",
                            headers={'X-BuildBetter-Api-Key': self.api_key},
                            json={'email': email}
                        )
                        if person_resp.ok:
                            person_id = person_resp.json()['id']
                    
                    # Build fields from mapping
                    fields = []
                    
                    if 'feedback' in mapping and row.get(mapping['feedback']):
                        fields.append({
                            'category': 'content',
                            'type': 'string',
                            'name': 'feedback',
                            'value': row[mapping['feedback']]
                        })
                    
                    if 'score' in mapping and row.get(mapping['score']):
                        fields.append({
                            'category': 'content',
                            'type': 'integer',
                            'name': 'score',
                            'value': int(row[mapping['score']])
                        })
                    
                    # Add any additional columns as metadata
                    for csv_col, field_name in mapping.items():
                        if csv_col not in ['email', 'feedback', 'score', 'date']:
                            if row.get(csv_col):
                                fields.append({
                                    'category': 'metadata',
                                    'type': 'string',
                                    'name': field_name,
                                    'value': row[csv_col]
                                })
                    
                    # Parse date
                    display_ts = datetime.now().isoformat()
                    if 'date' in mapping and row.get(mapping['date']):
                        try:
                            display_ts = datetime.strptime(
                                row[mapping['date']], 
                                '%Y-%m-%d %H:%M:%S'
                            ).isoformat()
                        except:
                            pass
                    
                    # Create feedback record
                    feedback_data = {
                        'display_ts': display_ts,
                        'external_id': f'csv-import-row-{row_num}',
                        'fields': fields
                    }
                    
                    if person_id:
                        feedback_data['person_id'] = person_id
                    
                    response = requests.post(
                        f"{self.base_url}/feedback-sources/{self.feedback_source_id}/records",
                        headers={'X-BuildBetter-Api-Key': self.api_key},
                        json=feedback_data
                    )
                    
                    if response.ok:
                        success_count += 1
                        print(f"✓ Row {row_num} imported")
                    else:
                        error_count += 1
                        print(f"✗ Row {row_num} failed: {response.text}")
                    
                    # Rate limiting
                    time.sleep(0.1)
                    
                except Exception as e:
                    error_count += 1
                    print(f"✗ Row {row_num} error: {str(e)}")
            
            print(f"\nImport complete: {success_count} successful, {error_count} errors")

# Usage
importer = CSVFeedbackImporter('ORGANIZATION_API_KEY', 123)
importer.import_csv('feedback_export.csv', {
    'email': 'Customer Email',
    'feedback': 'Comment',
    'score': 'NPS Score',
    'date': 'Submitted At',
    'product': 'Product Area',
    'account_type': 'Plan Type'
})

Error Handling & Retry Logic

Implement robust error handling for production systems.
class RobustFeedbackIngester {
  private maxRetries = 3;
  private retryDelay = 1000; // Start with 1 second
  
  async ingestWithRetry(
    feedbackData: any,
    attemptNum: number = 1
  ): Promise<any> {
    try {
      const response = await fetch(
        `https://api.buildbetter.app/v3/rest/feedback-sources/${this.feedbackSourceId}/records`,
        {
          method: 'POST',
          headers: {
            'X-BuildBetter-Api-Key': this.apiKey,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(feedbackData)
        }
      );
      
      if (response.status === 409) {
        // Duplicate entry - this is okay
        console.log(`Duplicate entry: ${feedbackData.external_id}`);
        return { status: 'duplicate' };
      }
      
      if (!response.ok) {
        const error = await response.text();
        throw new Error(`HTTP ${response.status}: ${error}`);
      }
      
      return await response.json();
      
    } catch (error) {
      console.error(`Attempt ${attemptNum} failed:`, error);
      
      if (attemptNum >= this.maxRetries) {
        // Log to error queue for manual review
        await this.logFailedIngestion(feedbackData, error);
        throw error;
      }
      
      // Exponential backoff
      const delay = this.retryDelay * Math.pow(2, attemptNum - 1);
      await new Promise(resolve => setTimeout(resolve, delay));
      
      return this.ingestWithRetry(feedbackData, attemptNum + 1);
    }
  }
  
  private async logFailedIngestion(data: any, error: any): Promise<void> {
    // Log to your error tracking system
    console.error('Failed to ingest after retries:', {
      data,
      error: error.message,
      timestamp: new Date().toISOString()
    });
    
    // Could also write to a dead letter queue for later processing
  }
}

Tips

  • Use external_id to prevent duplicate records
  • Handle errors gracefully - implement retry logic for transient failures
  • Store API keys securely in environment variables, not in code
  • Monitor success rates to catch issues early

Next Steps