# Call Recording

> Record voice agent calls for quality assurance, compliance, and training purposes.

Source: https://edesy.in/docs/voice-agent/features/recording

Record calls for quality assurance, compliance, dispute resolution, and AI training purposes.

## Configuration

### Enable Recording

```json
{
  "agent": {
    "name": "Customer Support",
    "recording": {
      "enabled": true,
      "format": "mp3",
      "channels": "dual",
      "storage": "s3",
      "retention_days": 90
    }
  }
}
```

### Recording Options

| Option | Values | Description |
|--------|--------|-------------|
| `enabled` | true/false | Enable recording |
| `format` | mp3, wav, ogg | Audio format |
| `channels` | mono, dual, stereo | Channel mode |
| `storage` | s3, gcs, azure | Storage provider |
| `retention_days` | 1-365 | Auto-deletion period |

## Channel Modes

### Mono (Single Channel)

Both parties mixed into one channel:

```
[Bot + User Mixed] ────────────────────────────►
```

Best for: Simple playback, smaller file size.

### Dual Channel

Separate channels for each party:

```
Channel 1 (Bot):   [Bot audio] ─────────────────►
Channel 2 (User):  [User audio] ────────────────►
```

Best for: Transcription, analysis, speaker separation.

### Stereo

True stereo with spatial separation:

```
Left:  [Bot audio]  ──────────────────────────►
Right: [User audio] ──────────────────────────►
```

Best for: Review, quality assurance.

## Implementation

### Recording Handler

```go
type CallRecorder struct {
    storage     Storage
    format      string
    sampleRate  int
    channels    int
}

func (r *CallRecorder) StartRecording(callID string) *Recording {
    return &Recording{
        CallID:    callID,
        StartTime: time.Now(),
        Encoder:   NewEncoder(r.format, r.sampleRate, r.channels),
        Buffer:    &bytes.Buffer{},
    }
}

func (r *CallRecorder) AddAudio(rec *Recording, audio []byte, source string) {
    if r.channels == 2 {
        // Dual channel - track source
        rec.Encoder.AddChannel(audio, source)
    } else {
        // Mono - mix together
        rec.Encoder.AddMixed(audio)
    }
}

func (r *CallRecorder) StopRecording(rec *Recording) (string, error) {
    // Finalize encoding
    data, err := rec.Encoder.Finalize()
    if err != nil {
        return "", err
    }

    // Generate storage path
    path := fmt.Sprintf("recordings/%s/%s.%s",
        rec.StartTime.Format("2006/01/02"),
        rec.CallID,
        r.format,
    )

    // Upload to storage
    url, err := r.storage.Upload(path, data)
    if err != nil {
        return "", err
    }

    return url, nil
}
```

### Integration with Pipeline

```go
func (p *Pipeline) processWithRecording(ctx context.Context) {
    if !p.agent.Recording.Enabled {
        p.processWithoutRecording(ctx)
        return
    }

    // Start recording
    recording := p.recorder.StartRecording(p.callID)
    defer func() {
        url, err := p.recorder.StopRecording(recording)
        if err != nil {
            log.Error("Recording failed: %v", err)
            return
        }
        p.storeRecordingURL(url)
    }()

    // Process with recording
    for {
        select {
        case audio := <-p.userAudio:
            p.recorder.AddAudio(recording, audio, "user")
            p.processUserAudio(audio)

        case audio := <-p.botAudio:
            p.recorder.AddAudio(recording, audio, "bot")
            p.sendBotAudio(audio)

        case <-ctx.Done():
            return
        }
    }
}
```

## Storage Providers

### Amazon S3

```json
{
  "recording": {
    "storage": "s3",
    "s3Config": {
      "bucket": "my-recordings-bucket",
      "region": "us-east-1",
      "prefix": "voice-agent/"
    }
  }
}
```

```go
type S3Storage struct {
    client *s3.Client
    bucket string
    prefix string
}

func (s *S3Storage) Upload(path string, data []byte) (string, error) {
    key := s.prefix + path

    _, err := s.client.PutObject(context.Background(), &s3.PutObjectInput{
        Bucket:      aws.String(s.bucket),
        Key:         aws.String(key),
        Body:        bytes.NewReader(data),
        ContentType: aws.String("audio/mpeg"),
    })
    if err != nil {
        return "", err
    }

    return fmt.Sprintf("s3://%s/%s", s.bucket, key), nil
}
```

### Google Cloud Storage

```json
{
  "recording": {
    "storage": "gcs",
    "gcsConfig": {
      "bucket": "my-recordings-bucket",
      "prefix": "voice-agent/"
    }
  }
}
```

### Azure Blob Storage

```json
{
  "recording": {
    "storage": "azure",
    "azureConfig": {
      "container": "recordings",
      "connectionString": "env:AZURE_STORAGE_CONNECTION"
    }
  }
}
```

## Compliance Features

### Recording Consent

```go
func (p *Pipeline) announceRecording() {
    if p.agent.Recording.RequireConsent {
        p.tts.Speak("This call may be recorded for quality assurance purposes.")
        time.Sleep(2 * time.Second)
    }
}

func (p *Pipeline) handleConsentResponse(transcript string) bool {
    negative := []string{"no", "don't record", "stop recording"}
    for _, phrase := range negative {
        if strings.Contains(strings.ToLower(transcript), phrase) {
            p.recorder.Disable()
            p.tts.Speak("Recording has been disabled for this call.")
            return false
        }
    }
    return true
}
```

### PCI-DSS Compliance

Pause recording during sensitive information:

```go
func (p *Pipeline) handleSensitiveData() {
    // Pause recording
    p.recorder.Pause()
    defer p.recorder.Resume()

    p.tts.Speak("For security, recording is paused. Please enter your card number.")

    // Collect sensitive data
    cardNumber := p.collectDTMF(16)

    // Resume recording
    p.tts.Speak("Thank you. Recording has resumed.")
}
```

### GDPR Compliance

```go
type RecordingMetadata struct {
    CallID        string
    CustomerPhone string
    ConsentGiven  bool
    Purpose       string
    RetentionDays int
    CreatedAt     time.Time
    ExpiresAt     time.Time
}

func (r *CallRecorder) StoreMetadata(meta RecordingMetadata) error {
    return r.db.Create(&meta)
}

// Data subject access request
func (r *CallRecorder) ExportRecordings(customerPhone string) ([]Recording, error) {
    return r.db.Find(&Recording{CustomerPhone: customerPhone})
}

// Right to erasure
func (r *CallRecorder) DeleteRecordings(customerPhone string) error {
    recordings, _ := r.ExportRecordings(customerPhone)
    for _, rec := range recordings {
        r.storage.Delete(rec.URL)
        r.db.Delete(&rec)
    }
    return nil
}
```

## Retention and Lifecycle

```go
type RetentionPolicy struct {
    DefaultDays   int
    ByCategory    map[string]int
    AutoDelete    bool
}

var policy = RetentionPolicy{
    DefaultDays: 90,
    ByCategory: map[string]int{
        "complaint":   365,
        "legal":       2555, // 7 years
        "training":    180,
        "standard":    90,
    },
    AutoDelete: true,
}

func (r *CallRecorder) ApplyRetention() {
    cutoff := time.Now().AddDate(0, 0, -policy.DefaultDays)

    expired, _ := r.db.Find(&Recording{
        ExpiresAt: lt(cutoff),
    })

    for _, rec := range expired {
        r.storage.Delete(rec.URL)
        r.db.Delete(&rec)
        log.Info("Deleted expired recording: %s", rec.CallID)
    }
}
```

## Accessing Recordings

### Via API

```bash
# Get the call, including recordingUrl (null when the call has no recording)
curl https://voice-agent.edesy.in/api/v1/calls/{conversationId} \
  -H "Authorization: Bearer $API_KEY"

# Download the recording
curl -o call.mp3 "$(curl -s https://voice-agent.edesy.in/api/v1/calls/{conversationId} \
  -H "Authorization: Bearer $API_KEY" | jq -r '.data.recordingUrl')"
```

See the [Calls API](https://edesy.in/docs/voice-agent/api/calls) for all call fields.

### Via Webhook

```json
{
  "event": "call.ended",
  "data": {
    "call_id": "call_abc123",
    "recording": {
      "id": "rec_xyz789",
      "url": "https://storage.example.com/recordings/call_abc123.mp3",
      "duration_seconds": 185,
      "size_bytes": 1485762,
      "format": "mp3"
    }
  }
}
```

## Best Practices

### 1. Dual Channel for Analysis

```json
{
  "recording": {
    "channels": "dual"
  }
}
```

Enables:
- Separate transcription per speaker
- Sentiment analysis per party
- Crosstalk detection
- Speaker diarization

### 2. Secure Storage

```go
// Encrypt at rest
config := S3Config{
    ServerSideEncryption: "aws:kms",
    KMSKeyID:             os.Getenv("KMS_KEY_ID"),
}

// Signed URLs for access
func (s *S3Storage) GetSignedURL(path string, expiry time.Duration) string {
    presigner := s3.NewPresignClient(s.client)
    req, _ := presigner.PresignGetObject(ctx, &s3.GetObjectInput{
        Bucket: aws.String(s.bucket),
        Key:    aws.String(path),
    }, func(opts *s3.PresignOptions) {
        opts.Expires = expiry
    })
    return req.URL
}
```

### 3. Metadata Tagging

```go
metadata := map[string]string{
    "call_id":     callID,
    "agent_id":    agentID,
    "customer_id": customerID,
    "duration":    fmt.Sprintf("%d", duration),
    "outcome":     outcome,
    "tags":        strings.Join(tags, ","),
}

s3Client.PutObject(&s3.PutObjectInput{
    Bucket:   bucket,
    Key:      key,
    Body:     data,
    Metadata: metadata,
})
```

## Next Steps

- [Transcripts](https://edesy.in/docs/voice-agent/features/transcripts) - Get call transcripts
- [Webhooks](https://edesy.in/docs/voice-agent/features/webhooks) - Recording notifications
- [Compliance](https://edesy.in/docs/voice-agent/advanced/security) - Security best practices
