> ## Documentation Index
> Fetch the complete documentation index at: https://docs.buildbetter.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Feedback Ingestion Examples

> Implementation examples for ingesting feedback data into BuildBetter

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.

<Tabs>
  <Tab title="Zendesk">
    ```javascript theme={null}
    // 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');
      }
    });
    ```
  </Tab>

  <Tab title="Intercom">
    ```python theme={null}
    # Intercom conversation sync (Python)
    import requests
    from datetime import datetime

    class IntercomIngester:
        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 sync_conversation(self, conversation):
            """Sync an Intercom conversation to BuildBetter"""
            
            # Create or update the person
            person_response = requests.put(
                f"{self.base_url}/people",
                headers={
                    "X-BuildBetter-Api-Key": self.api_key,
                    "Content-Type": "application/json"
                },
                json={
                    "email": conversation['user']['email'],
                    "first_name": conversation['user']['name'].split()[0],
                    "last_name": conversation['user']['name'].split()[-1],
                    "external_id": f"intercom-{conversation['user']['id']}"
                }
            )
            
            person_id = person_response.json()['id']
            
            # Ingest the conversation
            feedback_data = {
                "person_id": person_id,
                "display_ts": datetime.fromtimestamp(
                    conversation['created_at']
                ).isoformat(),
                "external_id": f"intercom-conv-{conversation['id']}",
                "fields": [
                    {
                        "category": "content",
                        "type": "string",
                        "name": "conversation",
                        "value": conversation['conversation_message']['body']
                    },
                    {
                        "category": "metadata",
                        "type": "string",
                        "name": "source",
                        "value": "intercom"
                    },
                    {
                        "category": "metadata",
                        "type": "string",
                        "name": "rating",
                        "value": str(conversation.get('conversation_rating', {}).get('rating', ''))
                    },
                    {
                        "category": "metadata",
                        "type": "string",
                        "name": "tags",
                        "value": ', '.join([tag['name'] for tag in conversation.get('tags', [])])
                    }
                ]
            }
            
            response = requests.post(
                f"{self.base_url}/feedback-sources/{self.feedback_source_id}/records",
                headers={
                    "X-BuildBetter-Api-Key": self.api_key,
                    "Content-Type": "application/json"
                },
                json=feedback_data
            )
            
            return response.json()

    # Usage
    ingester = IntercomIngester('ORGANIZATION_API_KEY', 123)
    ingester.sync_conversation(intercom_conversation_data)
    ```
  </Tab>

  <Tab title="Custom Support System">
    ```typescript theme={null}
    // TypeScript implementation for custom support system
    interface SupportTicket {
      id: string;
      customerId: string;
      subject: string;
      body: string;
      priority: 'low' | 'medium' | 'high' | 'urgent';
      category: string;
      createdAt: Date;
      resolvedAt?: Date;
    }

    class FeedbackIngester {
      private apiKey: string;
      private feedbackSourceId: number;
      
      constructor(apiKey: string, feedbackSourceId: number) {
        this.apiKey = apiKey;
        this.feedbackSourceId = feedbackSourceId;
      }
      
      async ingestTicket(ticket: SupportTicket): Promise<void> {
        const fields = [
          {
            category: 'content',
            type: 'string',
            name: 'subject',
            value: ticket.subject
          },
          {
            category: 'content',
            type: 'string',
            name: 'body',
            value: ticket.body
          },
          {
            category: 'metadata',
            type: 'string',
            name: 'priority',
            value: ticket.priority
          },
          {
            category: 'metadata',
            type: 'string',
            name: 'category',
            value: ticket.category
          }
        ];
        
        // Add resolution time if ticket is resolved
        if (ticket.resolvedAt) {
          fields.push({
            category: 'metadata',
            type: 'float',
            name: 'resolution_time_hours',
            value: (ticket.resolvedAt.getTime() - ticket.createdAt.getTime()) / 3600000
          });
        }
        
        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({
              person_id: ticket.customerId,
              display_ts: ticket.createdAt.toISOString(),
              external_id: `ticket-${ticket.id}`,
              fields
            })
          }
        );
        
        if (!response.ok) {
          throw new Error(`Failed to ingest ticket: ${response.statusText}`);
        }
      }
      
      async batchIngest(tickets: SupportTicket[]): Promise<void> {
        for (const ticket of tickets) {
          try {
            await this.ingestTicket(ticket);
            console.log(`✓ Ingested ticket ${ticket.id}`);
          } catch (error) {
            console.error(`✗ Failed to ingest ticket ${ticket.id}:`, error);
          }
        }
      }
    }
    ```
  </Tab>
</Tabs>

## NPS Survey Integration

Capture Net Promoter Score feedback to track customer satisfaction trends.

<Tabs>
  <Tab title="Delighted">
    ```javascript theme={null}
    // 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);
    };
    ```
  </Tab>

  <Tab title="Typeform">
    ```python theme={null}
    # Typeform NPS survey integration
    import requests
    from typing import Dict, Any

    class TypeformNPSIngester:
        def __init__(self, api_key: str, feedback_source_id: int):
            self.api_key = api_key
            self.feedback_source_id = feedback_source_id
            
        def process_typeform_response(self, response: Dict[str, Any]) -> None:
            """Process a Typeform NPS survey response"""
            
            # Extract answers from Typeform response
            answers = {
                answer['field']['ref']: answer['number'] 
                if answer['type'] == 'number' 
                else answer.get('text', answer.get('choice', {}).get('label', ''))
                for answer in response['answers']
            }
            
            # Map Typeform fields to our structure
            nps_score = answers.get('nps_score_field', 0)
            feedback_text = answers.get('feedback_field', '')
            email = answers.get('email_field', '')
            
            # Create or update person if email provided
            if email:
                person_resp = requests.put(
                    'https://api.buildbetter.app/v3/rest/people',
                    headers={'X-BuildBetter-Api-Key': self.api_key},
                    json={'email': email}
                )
                person_id = person_resp.json()['id']
            else:
                person_id = None
                
            # Prepare feedback fields
            fields = [
                {
                    'category': 'content',
                    'type': 'integer',
                    'name': 'nps_score',
                    'value': nps_score
                },
                {
                    'category': 'content',
                    'type': 'string',
                    'name': 'feedback',
                    'value': feedback_text
                },
                {
                    'category': 'metadata',
                    'type': 'string',
                    'name': 'survey_id',
                    'value': response['form_id']
                },
                {
                    'category': 'metadata',
                    'type': 'string',
                    'name': 'response_id',
                    'value': response['response_id']
                }
            ]
            
            # Add calculated metrics
            if nps_score >= 9:
                nps_category = 'promoter'
            elif nps_score >= 7:
                nps_category = 'passive'
            else:
                nps_category = 'detractor'
                
            fields.append({
                'category': 'metadata',
                'type': 'string',
                'name': 'nps_category',
                'value': nps_category
            })
            
            # Ingest the feedback
            payload = {
                'display_ts': response['submitted_at'],
                'external_id': f"typeform-{response['response_id']}",
                'fields': fields
            }
            
            if person_id:
                payload['person_id'] = person_id
                
            requests.post(
                f'https://api.buildbetter.app/v3/rest/feedback-sources/{self.feedback_source_id}/records',
                headers={'X-BuildBetter-Api-Key': self.api_key},
                json=payload
            )
    ```
  </Tab>
</Tabs>

## Slack Feedback Capture

Monitor Slack channels for product feedback and feature requests.

<Tabs>
  <Tab title="Slack Bot">
    ```javascript theme={null}
    // 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);
      }
    });
    ```
  </Tab>

  <Tab title="Slack Slash Command">
    ```python theme={null}
    # Slack slash command for submitting feedback
    from flask import Flask, request, jsonify
    import requests
    import hashlib
    import hmac
    import time

    app = Flask(__name__)

    def verify_slack_request(request):
        """Verify the request is from Slack"""
        slack_signing_secret = 'YOUR_SLACK_SIGNING_SECRET'
        timestamp = request.headers.get('X-Slack-Request-Timestamp', '')
        signature = request.headers.get('X-Slack-Signature', '')
        
        # Check timestamp to prevent replay attacks
        if abs(time.time() - float(timestamp)) > 60 * 5:
            return False
        
        # Verify signature
        sig_basestring = f"v0:{timestamp}:{request.get_data(as_text=True)}"
        my_signature = 'v0=' + hmac.new(
            slack_signing_secret.encode(),
            sig_basestring.encode(),
            hashlib.sha256
        ).hexdigest()
        
        return hmac.compare_digest(my_signature, signature)

    @app.route('/slack/feedback', methods=['POST'])
    def handle_feedback():
        if not verify_slack_request(request):
            return jsonify({'error': 'Invalid request'}), 403
        
        data = request.form
        
        # Parse the feedback command
        # Format: /feedback [category] feedback text
        text_parts = data['text'].split(' ', 1)
        if len(text_parts) == 2 and text_parts[0] in ['bug', 'feature', 'improvement']:
            category = text_parts[0]
            feedback_text = text_parts[1]
        else:
            category = 'general'
            feedback_text = data['text']
        
        # Ingest to BuildBetter
        feedback_data = {
            'display_ts': datetime.now().isoformat(),
            'external_id': f"slack-cmd-{data['response_url'].split('/')[-1]}",
            'fields': [
                {
                    'category': 'content',
                    'type': 'string',
                    'name': 'feedback',
                    'value': feedback_text
                },
                {
                    'category': 'metadata',
                    'type': 'string',
                    'name': 'category',
                    'value': category
                },
                {
                    'category': 'metadata',
                    'type': 'string',
                    'name': 'channel',
                    'value': data['channel_name']
                },
                {
                    'category': 'metadata',
                    'type': 'string',
                    'name': 'user',
                    'value': data['user_name']
                },
                {
                    'category': 'metadata',
                    'type': 'string',
                    'name': 'team',
                    'value': data['team_domain']
                }
            ]
        }
        
        response = requests.post(
            'https://api.buildbetter.app/v3/rest/feedback-sources/125/records',
            headers={'X-BuildBetter-Api-Key': 'ORGANIZATION_API_KEY'},
            json=feedback_data
        )
        
        if response.ok:
            return jsonify({
                'response_type': 'ephemeral',
                'text': f'✅ Feedback submitted as {category}'
            })
        else:
            return jsonify({
                'response_type': 'ephemeral',
                'text': '❌ Failed to submit feedback'
            })
    ```
  </Tab>
</Tabs>

## Product Analytics Events

Ingest product usage events and user behavior data.

<Tabs>
  <Tab title="Segment Integration">
    ```typescript theme={null}
    // 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
            })
          }
        );
      }
    }
    ```
  </Tab>

  <Tab title="Mixpanel">
    ```python theme={null}
    # Mixpanel event ingestion
    import hashlib
    from mixpanel import Mixpanel
    import requests

    class MixpanelFeedbackSync:
        def __init__(self, buildbetter_api_key, feedback_source_id):
            self.bb_api_key = buildbetter_api_key
            self.feedback_source_id = feedback_source_id
            
        def sync_feedback_events(self, events):
            """Sync Mixpanel feedback events to BuildBetter"""
            
            for event in events:
                # Filter for feedback-related events
                if event['event'] not in ['Feedback Sent', 'Feature Vote', 'Issue Report']:
                    continue
                    
                properties = event['properties']
                
                # Build fields array
                fields = [
                    {
                        'category': 'content',
                        'type': 'string',
                        'name': 'event_type',
                        'value': event['event']
                    }
                ]
                
                # Add feedback content if present
                if 'feedback_text' in properties:
                    fields.append({
                        'category': 'content',
                        'type': 'string',
                        'name': 'feedback',
                        'value': properties['feedback_text']
                    })
                
                # Add metadata
                metadata_fields = [
                    'distinct_id', 'mp_country_code', 'mp_region', 
                    'browser', 'os', 'device', 'feature_area',
                    'user_segment', 'account_type'
                ]
                
                for field in metadata_fields:
                    if field in properties:
                        fields.append({
                            'category': 'metadata',
                            'type': 'string',
                            'name': field,
                            'value': str(properties[field])
                        })
                
                # Generate unique ID
                event_id = hashlib.md5(
                    f"{event['event']}-{properties.get('time', '')}-{properties.get('distinct_id', '')}".encode()
                ).hexdigest()
                
                # Ingest to BuildBetter
                requests.post(
                    f'https://api.buildbetter.app/v3/rest/feedback-sources/{self.feedback_source_id}/records',
                    headers={'X-BuildBetter-Api-Key': self.bb_api_key},
                    json={
                        'display_ts': datetime.fromtimestamp(properties['time']).isoformat(),
                        'external_id': f'mixpanel-{event_id}',
                        'fields': fields
                    }
                )
    ```
  </Tab>
</Tabs>

## Email Feedback Processing

Process feedback received via email or contact forms.

```javascript theme={null}
// 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.

```python theme={null}
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.

```typescript theme={null}
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

<CardGroup>
  <Card title="API Reference" icon="book" href="/pages/api/feedback-ingestion">
    View the complete Feedback Ingestion API reference
  </Card>

  <Card title="Legacy GraphQL Examples" icon="graphql" href="/pages/api/graphql-examples">
    Reference legacy GraphQL examples during migration
  </Card>
</CardGroup>
