# Phone Finder Bulk

## Overview

[Bulks Phone Finder](https://tomba.io/bulks/phone-finder) enables you to discover phone numbers associated with email addresses, company websites, and LinkedIn profiles. Expand your contact methods and enable multi-channel outreach strategies with comprehensive phone number discovery.

### Key Features

- **Multi-Source Discovery**: Find phone numbers from emails, websites, and LinkedIn URLs
- **Bulk Processing**: Process up to 2,500 contacts per operation
- **Contact Expansion**: Add phone numbers to existing contact databases
- **Multi-Channel Outreach**: Enable phone-based sales and marketing activities
- **Quality Filtering**: Filter out invalid and non-business phone numbers

### How Phone Finder Bulk Works

1. **Prepare Contact List**: Compile emails, website URLs, or LinkedIn profile URLs
2. **Upload Data**: Provide contact information for phone number discovery
3. **Process Discovery**: Launch phone number search algorithms
4. **Review Results**: Examine discovered phone numbers with source context
5. **Export Contacts**: Download enriched contact lists with phone numbers

### Limitations

- Each Bulk is limited to 2,500 email addresses, websites, or LinkedIn profile URLs
- Webmail addresses (Gmail, Outlook, etc.) and disposable emails are skipped
- Special or unexpected characters may be removed from your file
- Duplicate and invalid lines will not be imported

## Go SDK Integration

### Installation

```bash
go get github.com/tomba-io/go
```

### Basic Setup

```go
package main

import (
    "fmt"
    "log"
    "time"
    "strings"

    "github.com/tomba-io/go/tomba"
    "github.com/tomba-io/go/tomba/models"
)

func main() {
    // Initialize Tomba client
    client := tomba.NewTomba("your-api-key", "your-secret-key")

    // Your phone finder code here
}
```

### Finding Phone Numbers from Email List

```go
// Email addresses for phone number discovery
emailList := []string{
    "contact@techcorp.com",
    "sales@startup.io",
    "info@enterprise.com",
    "support@business.net",
    "hello@company.org",
}

params := &models.BulkCreateParams{
    Name:    "Phone Discovery - Email Sources",
    List:    strings.Join(emailList, "\n"),
    Sources: true,   // Include sources for context
    Notifie: true,   // Notify when complete
}

// Create phone finder bulk operation
response, err := client.CreateBulk(models.BulkTypePhoneFinder, params)
if err != nil {
    log.Fatal("Failed to create phone finder bulk:", err)
}

bulkID := *response.Data.ID
fmt.Printf("Created phone finder bulk with ID: %d\n", bulkID)
```

### Finding Phone Numbers from Website URLs

```go
// Company websites for phone discovery
websites := []string{
    "https://techcorp.com",
    "https://startup.io",
    "https://enterprise.com",
    "https://business.net",
    "https://company.org",
}

params := &models.BulkCreateParams{
    Name:    "Phone Discovery - Company Websites",
    List:    strings.Join(websites, "\n"),
    Sources: true,
    Notifie: true,
}

response, err := client.CreateBulk(models.BulkTypePhoneFinder, params)
if err != nil {
    log.Fatal("Failed to create phone finder bulk:", err)
}

bulkID := *response.Data.ID
fmt.Printf("Created website phone finder bulk: %d\n", bulkID)
```

### Creating Phone Finder from CSV File

```go
// Create phone finder from CSV with mixed sources
params := &models.BulkCreateParams{
    Name:      "Multi-Channel Phone Discovery",
    Delimiter: ",",
    Column:    2,      // Source column (email/website/linkedin URL)
    Sources:   true,
    Notifie:   true,
}

// CSV can contain emails, websites, or LinkedIn URLs
response, err := client.CreateBulkWithFile(
    models.BulkTypePhoneFinder,
    params,
    "/path/to/contact-sources.csv",
)
if err != nil {
    log.Fatal("Failed to create bulk with file:", err)
}

bulkID := *response.Data.ID
fmt.Printf("Created phone finder bulk: %d\n", bulkID)
```

### Launching and Monitoring Phone Discovery

```go
// Launch the phone discovery process
launchResponse, err := client.LaunchBulk(models.BulkTypePhoneFinder, bulkID)
if err != nil {
    log.Fatal("Failed to launch phone finder:", err)
}

fmt.Println("Phone number discovery started!")

// Monitor progress
startTime := time.Now()
for {
    progress, err := client.GetBulkProgress(models.BulkTypePhoneFinder, bulkID)
    if err != nil {
        log.Printf("Error checking progress: %v", err)
        time.Sleep(30 * time.Second)
        continue
    }

    elapsed := time.Since(startTime).Round(time.Second)
    fmt.Printf("Phone discovery: %d%% (%d processed) - %v elapsed\n",
        progress.Progress,
        progress.Processed,
        elapsed,
    )

    if progress.Status {
        fmt.Println("Phone number discovery completed!")
        break
    }

    // Check every 45 seconds (phone discovery can be slower)
    time.Sleep(45 * time.Second)
}
```

### Retrieving Phone Discovery Results

```go
// Get detailed results
bulk, err := client.GetBulk(models.BulkTypePhoneFinder, bulkID)
if err != nil {
    log.Fatal("Failed to get bulk details:", err)
}

bulkInfo := bulk.Data[0]
fmt.Printf("Phone Discovery Results:\n")
fmt.Printf("- Campaign: %s\n", bulkInfo.Name)
fmt.Printf("- Status: %v\n", bulkInfo.Status)
fmt.Printf("- Sources Processed: %d\n", bulkInfo.Processed)

// Download results with phone numbers
err = client.SaveBulkResults(
    models.BulkTypePhoneFinder,
    bulkID,
    "phone-numbers.csv",
    "full",
)
if err != nil {
    log.Fatal("Failed to download results:", err)
}

fmt.Println("Phone numbers saved to phone-numbers.csv")

// Download only records where phones were found
err = client.SaveBulkResults(
    models.BulkTypePhoneFinder,
    bulkID,
    "found-phones.csv",
    "valid",
)
if err != nil {
    log.Printf("Warning: Could not save found phones: %v", err)
} else {
    fmt.Println("Found phone numbers saved to found-phones.csv")
}
```

### Managing Phone Finder Operations

```go
// List all phone finder bulks
params := &models.BulkGetParams{
    Page:      1,
    Limit:     15,
    Direction: "desc",
    Filter:    "all",
}

bulks, err := client.GetAllPhoneFinderBulks(params)
if err != nil {
    log.Fatal("Failed to get phone finder bulks:", err)
}

fmt.Printf("Phone Finder Operations:\n")
for _, bulk := range bulks.Data {
    status := "In Progress"
    if bulk.Status {
        status = "Completed"
    }

    fmt.Printf("- %s (ID: %d)\n", bulk.Name, bulk.BulkID)
    fmt.Printf("  Status: %s | Progress: %d%% | Processed: %d\n",
        status, bulk.Progress, bulk.Processed)

    created := bulk.CreatedAt.Format("2006-01-02 15:04")
    fmt.Printf("  Created: %s\n", created)
    fmt.Println()
}

// Archive old completed operations
for _, bulk := range bulks.Data {
    if bulk.Status && time.Since(bulk.CreatedAt) > 7*24*time.Hour { // 7 days old
        _, err := client.ArchiveBulk(models.BulkTypePhoneFinder, bulk.BulkID)
        if err != nil {
            log.Printf("Failed to archive bulk %d: %v", bulk.BulkID, err)
        } else {
            fmt.Printf("Archived old phone finder: %s\n", bulk.Name)
        }
    }
}
```

### Advanced Phone Discovery Workflow

```go
type PhoneDiscoveryResult struct {
    Source      string
    SourceType  string // "email", "website", "linkedin"
    PhoneNumber string
    Found       bool
    Country     string
    Type        string // "mobile", "landline", etc.
}

func discoverPhonesFromMixedSources(client *tomba.Tomba, sources []string) ([]PhoneDiscoveryResult, error) {
    // Step 1: Categorize sources
    var emails, websites, linkedinURLs []string

    for _, source := range sources {
        source = strings.TrimSpace(source)
        if strings.Contains(source, "@") {
            emails = append(emails, source)
        } else if strings.Contains(source, "linkedin.com") {
            linkedinURLs = append(linkedinURLs, source)
        } else if strings.HasPrefix(source, "http") {
            websites = append(websites, source)
        }
    }

    log.Printf("Categorized sources: %d emails, %d websites, %d LinkedIn profiles",
        len(emails), len(websites), len(linkedinURLs))

    var allResults []PhoneDiscoveryResult

    // Step 2: Process each category separately for better tracking
    if len(emails) > 0 {
        results, err := processPhoneDiscoveryBatch(client, emails, "email")
        if err != nil {
            log.Printf("Email phone discovery failed: %v", err)
        } else {
            allResults = append(allResults, results...)
        }
    }

    if len(websites) > 0 {
        results, err := processPhoneDiscoveryBatch(client, websites, "website")
        if err != nil {
            log.Printf("Website phone discovery failed: %v", err)
        } else {
            allResults = append(allResults, results...)
        }
    }

    if len(linkedinURLs) > 0 {
        results, err := processPhoneDiscoveryBatch(client, linkedinURLs, "linkedin")
        if err != nil {
            log.Printf("LinkedIn phone discovery failed: %v", err)
        } else {
            allResults = append(allResults, results...)
        }
    }

    return allResults, nil
}

func processPhoneDiscoveryBatch(client *tomba.Tomba, sources []string, sourceType string) ([]PhoneDiscoveryResult, error) {
    // Respect the 2,500 limit
    if len(sources) > 2500 {
        log.Printf("Warning: Truncating %s sources from %d to 2,500", sourceType, len(sources))
        sources = sources[:2500]
    }

    // Create batch operation
    params := &models.BulkCreateParams{
        Name:    fmt.Sprintf("Phone Discovery - %s sources - %s",
            strings.Title(sourceType), time.Now().Format("15:04:05")),
        List:    strings.Join(sources, "\n"),
        Sources: true,
        Notifie: false,
    }

    response, err := client.CreateBulk(models.BulkTypePhoneFinder, params)
    if err != nil {
        return nil, fmt.Errorf("failed to create phone finder bulk: %w", err)
    }

    bulkID := *response.Data.ID
    log.Printf("Created %s phone discovery bulk: %d", sourceType, bulkID)

    // Launch and monitor
    _, err = client.LaunchBulk(models.BulkTypePhoneFinder, bulkID)
    if err != nil {
        return nil, fmt.Errorf("failed to launch bulk: %w", err)
    }

    // Wait for completion with timeout
    timeout := time.After(45 * time.Minute)
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-timeout:
            return nil, fmt.Errorf("phone discovery timed out for %s sources", sourceType)

        case <-ticker.C:
            progress, err := client.GetBulkProgress(models.BulkTypePhoneFinder, bulkID)
            if err != nil {
                log.Printf("Error checking progress: %v", err)
                continue
            }

            if progress.Progress%25 == 0 || progress.Status {
                log.Printf("%s phone discovery: %d%% (%d processed)",
                    strings.Title(sourceType), progress.Progress, progress.Processed)
            }

            if progress.Status {
                // Parse results
                return parsePhoneResults(client, bulkID, sourceType)
            }
        }
    }
}

func parsePhoneResults(client *tomba.Tomba, bulkID int64, sourceType string) ([]PhoneDiscoveryResult, error) {
    // Download results
    data, err := client.DownloadBulk(models.BulkTypePhoneFinder, bulkID,
        &models.BulkDownloadParams{Type: "full"})
    if err != nil {
        return nil, fmt.Errorf("failed to download results: %w", err)
    }

    // Parse CSV results (simplified parsing)
    var results []PhoneDiscoveryResult
    lines := strings.Split(string(data), "\n")

    for i, line := range lines {
        if i == 0 || strings.TrimSpace(line) == "" {
            continue // Skip header and empty lines
        }

        // Simple CSV parsing - use proper CSV parser in production
        parts := strings.Split(line, ",")
        if len(parts) >= 2 {
            result := PhoneDiscoveryResult{
                Source:     strings.Trim(parts[0], `"`),
                SourceType: sourceType,
                Found:      false,
            }

            if len(parts) > 1 && strings.TrimSpace(parts[1]) != "" {
                result.PhoneNumber = strings.Trim(parts[1], `"`)
                result.Found = true
            }

            // Extract additional metadata if available
            if len(parts) > 2 {
                result.Country = strings.Trim(parts[2], `"`)
            }
            if len(parts) > 3 {
                result.Type = strings.Trim(parts[3], `"`)
            }

            results = append(results, result)
        }
    }

    log.Printf("Parsed %d %s phone results (%d found)",
        len(results), sourceType, countFoundPhones(results))

    return results, nil
}

func countFoundPhones(results []PhoneDiscoveryResult) int {
    count := 0
    for _, result := range results {
        if result.Found {
            count++
        }
    }
    return count
}

// Complete workflow example
func runPhoneDiscoveryCampaign(client *tomba.Tomba, csvPath string) error {
    // Read sources from CSV
    file, err := os.Open(csvPath)
    if err != nil {
        return fmt.Errorf("failed to open CSV: %w", err)
    }
    defer file.Close()

    reader := csv.NewReader(file)
    records, err := reader.ReadAll()
    if err != nil {
        return fmt.Errorf("failed to read CSV: %w", err)
    }

    var sources []string
    for i, record := range records {
        if i == 0 || len(record) == 0 { // Skip header and empty rows
            continue
        }
        sources = append(sources, record[0])
    }

    log.Printf("Processing phone discovery for %d sources", len(sources))

    // Discover phones
    results, err := discoverPhonesFromMixedSources(client, sources)
    if err != nil {
        return fmt.Errorf("phone discovery failed: %w", err)
    }

    // Analyze results
    totalSources := len(results)
    phonesFound := countFoundPhones(results)

    // Group by source type
    typeStats := make(map[string]struct{ Total, Found int })
    for _, result := range results {
        stats := typeStats[result.SourceType]
        stats.Total++
        if result.Found {
            stats.Found++
        }
        typeStats[result.SourceType] = stats
    }

    // Report results
    fmt.Printf("\nPhone Discovery Campaign Results:\n")
    fmt.Printf("Total Sources: %d\n", totalSources)
    fmt.Printf("Phones Found: %d\n", phonesFound)
    fmt.Printf("Success Rate: %.1f%%\n", float64(phonesFound)/float64(totalSources)*100)
    fmt.Printf("\nBreakdown by Source Type:\n")

    for sourceType, stats := range typeStats {
        successRate := float64(stats.Found) / float64(stats.Total) * 100
        fmt.Printf("- %s: %d/%d (%.1f%%)\n",
            strings.Title(sourceType), stats.Found, stats.Total, successRate)
    }

    // Save consolidated results
    timestamp := time.Now().Format("20060102-150405")
    outputFile := fmt.Sprintf("phone-discovery-results-%s.csv", timestamp)

    return savePhoneResults(results, outputFile)
}

func savePhoneResults(results []PhoneDiscoveryResult, filename string) error {
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()

    writer := csv.NewWriter(file)
    defer writer.Flush()

    // Write header
    writer.Write([]string{"Source", "Source_Type", "Phone_Number", "Found", "Country", "Type"})

    // Write results
    for _, result := range results {
        writer.Write([]string{
            result.Source,
            result.SourceType,
            result.PhoneNumber,
            fmt.Sprintf("%v", result.Found),
            result.Country,
            result.Type,
        })
    }

    log.Printf("Saved phone discovery results to: %s", filename)
    return nil
}

// Usage example
func main() {
    client := tomba.NewTomba("your-api-key", "your-secret-key")

    err := runPhoneDiscoveryCampaign(client, "mixed-sources.csv")
    if err != nil {
        log.Fatal("Phone discovery campaign failed:", err)
    }
}
```

### CSV File Format Examples

**Mixed Sources**

```text
source
john.doe@example.com
https://techcorp.com
https://www.linkedin.com/in/jane-smith
contact@startup.io
https://business.net
support@company.org
```

**With Source Type Labels**

```text
source,notes
john.doe@example.com,Sales contact
https://techcorp.com,website,Company
https://www.linkedin.com/in/jane-smith,linkedin,CTO profile
contact@startup.io,General inquiry
https://business.net/contact,Contact page
```

## Best Practices

- **Business Focus**: Prioritize business contacts over personal ones
- **Data Quality**: Use verified email addresses and legitimate websites as sources
- **Compliance**: Ensure compliance with telecommunications regulations and privacy laws
- **Multi-Channel Strategy**: Integrate phone data with email and social outreach
- **Regular Updates**: Refresh phone data as contact information changes
- **Source Validation**: Validate input sources before processing
- **Batch Management**: Process sources in manageable batches under 2,500 limit
- **Result Analysis**: Track success rates by source type to optimize strategies
- **Privacy Respect**: Use discovered phone numbers responsibly and ethically
- **Quality Control**: Verify phone numbers before using for outreach campaigns
