Overview
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
Prepare Contact List : Compile emails, website URLs, or LinkedIn profile URLs
Upload Data : Provide contact information for phone number discovery
Process Discovery : Launch phone number search algorithms
Review Results : Examine discovered phone numbers with source context
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
go get github.com/tomba-io/go
Basic Setup
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
// 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
// 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
// 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
// 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
// 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
// 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
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 ( " \n Phone 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 ( " \n Breakdown 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
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
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
Last modified on October 10, 2025