Enrich Webinar Leads and Create Outreach with Surfe API: Python Code Tutorial

Your team spent hours running the webinar—don’t waste another second chasing incomplete leads.
This Python tutorial shows you how to turn Zoom registrants into enriched, sequenced prospects—faster than ever.
Using Surfe’s Contact Enrichment API, you’ll fetch Zoom webinar registrants, enrich them with verified contact details, and create new Outreach prospects—all in one streamlined script. It’s the perfect workflow for sales teams looking to act fast on inbound webinar leads without losing hours to repetitive tasks.
By the end of this tutorial, you’ll have a fully functioning Python script that:
-
Fetches webinar registrants directly from Zoom
-
Extracts key information for lead qualification
-
Enriches registrant data with verified emails and phone numbers using Surfe’s API
-
Creates prospects in Outreach directly
-
Adds prospects to your sales sequence for immediate follow-up
Let’s get into it.
Want the full script?Jump to the bottom or view it on GitHub!
Prerequisites
- Python 3.x Installation
Most modern operating systems come with Python 3 pre-installed. To check if Python is installed on your system:
- Windows: Open Command Prompt (Win + R, type cmd, press Enter) and run:
py --version
- macOS/Linux: Open Terminal and run:
python3 --version
If Python is not installed, download it from the official Python website and follow the installation instructions for your OS.
- Basic Python Programming Knowledge
- Zoom Account with API Access
To fetch webinar registrants, you’ll need a Zoom account with API access:
- Create a Zoom app in your Zoom Marketplace account
- Generate either a JWT token or OAuth credentials
- Note your webinar ID from a webinar with registrants
- Surfe Account and API Key
To enrich your leads, you’ll need a Surfe account and API key. You can find the API documentation and instructions for generating your API key in the Surfe Developer Docs.
- Outreach Account with API Access
To create prospects and add them to sequences:
- Set up OAuth access in your Outreach account
- Obtain your access token
- Have a sequence ID and mailbox ID ready for prospect assignment
Step 1: Setting Up Your Environment
Let’s begin by setting up your development environment and installing the necessary dependencies.
Creating a Virtual Environment (Optional but Recommended)
Creating a virtual environment is recommended to keep your project dependencies organized:
# macOS/Linuxpython3 -m venv env
source env/bin/activate
# Windowspy -m venv env
env\\Scripts\\activate
Installing Required Packages
Install the necessary Python packages:
# macOS/Linux
python3 -m pip install requests python-dotenv
# Windows
py -m pip install requests python-dotenv
Storing Your API Keys Securely
Never hardcode API keys in your script. Instead, store them as environment variables.
Create a file named .env
in your project’s root directory:
ZOOM_API_TOKEN=your_zoom_api_token
ZOOM_WEBINAR_ID=your_webinar_id
SURFE_API_KEY=your_surfe_api_key
OUTREACH_ACCESS_TOKEN=your_outreach_access_token
OUTREACH_SEQUENCE_ID=your_sequence_id
OUTREACH_MAILBOX_ID=your_mailbox_id
Create a Python script file named main.py and add the necessary imports, then create a main function and load all the environment variables:
import os
import sys
import time
from dotenv import load_dotenv
def main():
# Load environment variables
load_dotenv()
zoom_api_token = os.getenv("ZOOM_API_TOKEN")
surfe_api_key = os.getenv("SURFE_API_KEY")
outreach_access_token = os.getenv("OUTREACH_ACCESS_TOKEN")
# Get required IDs from environment variables
webinar_id = os.getenv("ZOOM_WEBINAR_ID")
sequence_id = os.getenv("OUTREACH_SEQUENCE_ID")
mailbox_id = os.getenv("OUTREACH_MAILBOX_ID")
# Validate required environment variables
if not all([zoom_api_token, surfe_api_key, outreach_access_token, webinar_id, sequence_id, mailbox_id]):
print("Error: Missing required environment variables. Please check your .env file.")
return
Step 2: Fetching Webinar Registrants from Zoom – Creating a Zoom Service
We will kick off our project by creating a Zoom service containing a variety of functions and utilities specifically concerned with the Zoom Meeting API to help us organise and visualize our logic. Here is how we initialize it:
class ZoomService:
def __init__(self, api_token, api_base_url="<https://api.zoom.us/v2>"):
self.api_token = api_token
self.api_base_url = api_base_url
The first component we will build is a method that makes requests to Zoom API while always adding the the api_token to the headers and formatting the endpoint URLs and methods. this will help us make the following components simpler and avoid repeating the same steps:
def _make_request(self, method, endpoint, params=None, data=None, json_data=None):
url = urljoin(self.api_base_url, endpoint)
headers = {
"Authorization": f"Bearer {self.api_token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = json.dumps(json_data) if json_data else data
response = requests.request(
method=method,
url=url,
params=params,
headers=headers,
data=payload
)
response.raise_for_status()
return response.json()
Next, the following function connects to Zoom’s API and retrieves all registrants for a specific webinar. The get_webinar_registrants
functions is for handling pagination automatically to ensure we capture all registrants, regardless of the webinar size:
def get_webinar_registrants(self, webinar_id, page_size=100, next_page_token=None):
endpoint = f"webinars/{webinar_id}/registrants"
params = {
"page_size": page_size
}
if next_page_token:
params["next_page_token"] = next_page_token
response = self._make_request("GET", endpoint, params=params)
return response
def get_all_webinar_registrants(self, webinar_id, page_size=100):
all_registrants = []
next_page_token = None
while True:
response = self.get_webinar_registrants(webinar_id, page_size, next_page_token)
registrants = response.get("registrants", [])
all_registrants.extend(registrants)
next_page_token = response.get("next_page_token")
if not next_page_token:
break
return all_registrants
Not all registrant data is suitable for enrichment. We need to filter and process the data to extract contacts with sufficient information for lead qualification.
The sufficient data for using the enrichment API is either:
- linkedinUrl OR
- firstName + lastName + companyName OR
- firstName + lastName + companyDomain
This function processes the raw registrant data from Zoom to extract essential information like names and company details. Registrants without complete information are filtered out to ensure quality leads:
def process_webinar_registrants(self, registrants):
processed_registrants = []
for registrant in registrants:
# Skip registrants without first name, last name, or company
if not (registrant.get("first_name") and registrant.get("last_name") and (registrant.get("org") or registrant.get("email"))):
continue
if registrant.get("email"):
companyDomain = registrant.get("email").split("@")[1]
else:
companyDomain = None
processed_registrants.append({
"firstName": registrant.get("first_name", ""),
"lastName": registrant.get("last_name", ""),
"companyName": registrant.get("org", ""),
"companyDomain": companyDomain
})
return processed_registrants
Lastly, a method that will fetch the webinar’s full details from zoom to add more data to the prospect:
def get_webinar_details(self, webinar_id):
endpoint = f"webinars/{webinar_id}"
response = self._make_request("GET", endpoint)
return response
Step 3: Enriching Leads with Surfe API – Creating a Surfe service
Now we’ll enrich the processed registrant data using Surfe’s People Enrichment API to get verified email addresses, phone numbers, and additional information.
This is where the magic happens. We send the processed registrant data to Surfe’s API for enrichment. The API will return verified contact information including email addresses, phone numbers, job titles, and more.
Just like Zoom, we will create a similar service for Surfe methods that will be used to build this script. This is how we will initialize our service:
class SurfeService:
def __init__(self, api_key, version="v2"):
self.api_key = api_key
self.base_url = f"<https://api.surfe.com/{version}>"
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
First method we will create is one that prepares the payload for calling the Surfe enrichment API:
def prepare_people_payload(self, people_data):
return {
"include": {
"email": True,
"mobile": True,
"linkedInUrl": True,
},
"people": people_data
}
Next is a method that sends a POST request to /v2/people/enrich
to start the enrichment process:
def start_enrichment(self, payload):
url = f"{self.base_url}/people/enrich"
response = requests.post(url, headers=self.headers, json=payload)
response.raise_for_status()
data = response.json()
return data["id"]
Surfe’s bulk enrichment process operates asynchronously, meaning we need to poll the API to check when the process is completed and retrieve the results. The last method is one that Monitors the status of an enrichment request until completion or failure:
def poll_enrichment_status(self, enrichment_id, max_attempts=60, delay=5):
url = f"{self.base_url}/people/enrich/{enrichment_id}"
attempts = 0
while attempts < max_attempts:
response = requests.get(url, headers=self.headers)
response.raise_for_status()
data = response.json()
status = data.get("status")
if status == "COMPLETED":
return data
elif status == "FAILED":
raise Exception(f"Enrichment failed: {data.get('error', 'Unknown error')}")
print(f"Enrichment status: {status}. Waiting {delay} seconds...")
time.sleep(delay)
attempts += 1
raise Exception("Enrichment timed out")
Step 4: Adding Prospects in Outreach – Creating an Outreach Service
Same as before, we’ll start initializing a service the holds all helper methods for the Outreach API:
class OutreachService:
def __init__(self, access_token, api_base_url="<https://api.outreach.io/api/v2>"):
self.access_token = access_token
self.api_base_url = api_base_url
Now, for each enriched contact we will need to format its new data for Outreach, this function simply maps Surfe’s enrichment response to the Outreach prospect API endpoint:
def format_enriched_data_for_outreach(self, enriched_person, event_name):
outreach_data = {
"firstName": enriched_person.get("firstName", ""),
"lastName": enriched_person.get("lastName", ""),
"company": enriched_person.get("companyName", ""),
"title": enriched_person.get("title", ""),
"occupation": enriched_person.get("jobTitle", ""),
"linkedinUrl": enriched_person.get("linkedinUrl", ""),
"websiteUrl1": enriched_person.get("companyDomain", ""),
"tags": enriched_person.get("department", []) + enriched_person.get("seniorities", []),
"event": event_name
}
# Add all valid emails
if enriched_person.get("emails") and len(enriched_person["emails"]) > 0:
# Filter for valid emails and extract email addresses
valid_emails = [
email.get("email")
for email in enriched_person["emails"]
if email.get("validationStatus") == "VALID" and email.get("email")
]
if valid_emails:
outreach_data["emails"] = valid_emails
# Add phone if available
if enriched_person.get("mobilePhones") and len(enriched_person["mobilePhones"]) > 0:
# Sort by confidence score
sorted_phones = sorted(
enriched_person["mobilePhones"],
key=lambda x: x.get("confidenceScore", 0),
reverse=True
)
if sorted_phones:
outreach_data["phones"] = [
phone.get("mobilePhone")
for phone in sorted_phones
if phone.get("mobilePhone")
]
return outreach_data
Afterwards, we need a function that checks if this contact already exists in the Outreach prospects list using the e-mail provided from Surfe:
def find_prospect_by_email(self, email):
endpoint = "prospects"
params = {
"filter[emails]": email
}
response = self._make_request("GET", endpoint, params=params)
if response.get("data") and len(response["data"]) > 0:
return response["data"][0]
return None
After filtering out the existing prospects we will use this function to create new prospects:
def create_prospect(self, prospect_data):
endpoint = "prospects"
# Format data for Outreach API (JSON:API format)
json_data = {
"data": {
"type": "prospect",
"attributes": prospect_data
}
}
response = self._make_request("POST", endpoint, json_data=json_data)
return response
The final step is adding the prospects to your Outreach sequence for automated follow-up:
def add_prospect_to_sequence(self, prospect_id, sequence_id, mailbox_id):
endpoint = "sequenceStates"
# Format data for Outreach API (JSON:API format)
json_data = {
"data": {
"type": "sequenceState",
"relationships": {
"prospect": {
"data": {
"type": "prospect",
"id": prospect_id
}
},
"sequence": {
"data": {
"type": "sequence",
"id": sequence_id
}
},
"mailbox": {
"data": {
"type": "mailbox",
"id": mailbox_id
}
}
}
}
}
response = self._make_request("POST", endpoint, json_data=json_data)
return response
Step 5: Putting It All Together
Now it is time to start putting together all the components we just created to build the full script. we go back to the main.py file and build the rest of the script.
The following illustration helps explain better what the flow looks like:

def main():
# Load environment variables
load_dotenv()
zoom_api_token = os.getenv("ZOOM_API_TOKEN")
surfe_api_key = os.getenv("SURFE_API_KEY")
outreach_access_token = os.getenv("OUTREACH_ACCESS_TOKEN")
# Get required IDs from environment variables
webinar_id = os.getenv("ZOOM_WEBINAR_ID")
sequence_id = os.getenv("OUTREACH_SEQUENCE_ID")
mailbox_id = os.getenv("OUTREACH_MAILBOX_ID")
# Validate required environment variables
if not all([zoom_api_token, surfe_api_key, outreach_access_token, webinar_id, sequence_id, mailbox_id]):
print("Error: Missing required environment variables. Please check your .env file.")
return
try:
# Initialize services
zoom_service = ZoomService(zoom_api_token)
surfe_service = SurfeService(surfe_api_key, version="v2")
outreach_service = OutreachService(outreach_access_token)
# Step 1: Fetch webinar registrants from Zoom
print(f"Fetching registrants for webinar {webinar_id}...")
registrants = zoom_service.get_all_webinar_registrants(webinar_id)
print(f"Found {len(registrants)} registrants for the webinar")
# Step 2: Process registrants to extract essential information
print("Processing registrants to extract name and company information...")
processed_registrants = zoom_service.process_webinar_registrants(registrants)
print(f"Found {len(processed_registrants)} registrants with valid name and company information")
if not processed_registrants:
print("No registrants with valid information found. Exiting...")
return
# Step 3: Prepare and start Surfe enrichment
print("Preparing Surfe enrichment...")
surfe_payload = surfe_service.prepare_people_payload(processed_registrants)
print("Starting Surfe enrichment process...")
enrichment_id = surfe_service.start_enrichment(surfe_payload)
print(f"Enrichment started with ID: {enrichment_id}")
# Step 4: Poll for enrichment results
print("Polling for enrichment results...")
enriched_data = surfe_service.poll_enrichment_status(enrichment_id)
print("Enrichment completed successfully")
enriched_people = enriched_data.get("people", [])
print(f"Enriched {len(enriched_people)} registrants")
# Step 5: Create prospects in Outreach and add to sequence
print("Adding enriched registrants to Outreach...")
event_name = zoom_service.get_webinar_details(webinar_id).get("topic")
success_count = 0
for person in enriched_people:
try:
# Format the enriched data for Outreach
prospect_data = outreach_service.format_enriched_data_for_outreach(person, event_name)
# Check if we have a valid email
if not prospect_data.get("emails"):
print(f"Skipping {person.get('firstName')} {person.get('lastName')} - No valid email found")
continue
# Check if prospect already exists in Outreach
existing_prospect = outreach_service.find_prospect_by_email(prospect_data["emails"])
if existing_prospect:
prospect_id = existing_prospect["id"]
print(f"Prospect {person.get('firstName')} {person.get('lastName')} already exists with ID: {prospect_id}")
else:
# Create a new prospect
print(f"Creating prospect for {person.get('firstName')} {person.get('lastName')}...")
prospect_response = outreach_service.create_prospect(prospect_data)
prospect_id = prospect_response["data"]["id"]
print(f"Created prospect with ID: {prospect_id}")
# Add the prospect to the sequence
print(f"Adding prospect {prospect_id} to sequence {sequence_id}...")
outreach_service.add_prospect_to_sequence(prospect_id, sequence_id, mailbox_id)
print(f"Successfully added prospect to sequence")
success_count += 1
except Exception as e:
print(f"Error processing {person.get('firstName')} {person.get('lastName')}: {str(e)}")
print(f"Successfully added {success_count} prospects to Outreach sequence {sequence_id}")
except Exception as e:
print(f"Error: {str(e)}")
Step 6: Running the Script
Once you’ve set up your environment variables and created the script, you can run it:
Now that you have your complete script, Here’s how to execute the script and what to expect during its operation.
- Ensure your .env file contains all necessary variables
# Zoom API credentials
ZOOM_API_TOKEN=your_zoom_jwt_or_oauth_token_here
ZOOM_WEBINAR_ID=your_webinar_id_here
# Surfe API credentials
SURFE_API_KEY=your_surfe_api_key_here
# Outreach API credentials
OUTREACH_ACCESS_TOKEN=your_outreach_oauth_token_here
OUTREACH_SEQUENCE_ID=your_outreach_sequence_id_here
OUTREACH_MAILBOX_ID=your_outreach_mailbox_id_here
- Open your terminal or command prompt, navigate to the directory containing your script, and run:
python main.py
Expected Output
When you run the script, you will see a series of status messages in the console that help you track its progress:

Complete Code for Easy Integration
"""
Zoom Webinar Lead Generation and Outreach Integration
This script fetches webinar registrants from Zoom, enriches them using Surfe API,
and adds them to an Outreach sequence for follow-up.
"""
import os
import sys
import time
from dotenv import load_dotenv
# Add core directory to path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
# Import core services
from core.surfe import SurfeService
from core.integrations.zoom import ZoomService
from core.integrations.outreach import OutreachService
def main():
"""
Main function to orchestrate the integration process
"""
# Load environment variables
load_dotenv()
zoom_api_token = os.getenv("ZOOM_API_TOKEN")
surfe_api_key = os.getenv("SURFE_API_KEY")
outreach_access_token = os.getenv("OUTREACH_ACCESS_TOKEN")
# Get required IDs from environment variables
webinar_id = os.getenv("ZOOM_WEBINAR_ID")
sequence_id = os.getenv("OUTREACH_SEQUENCE_ID")
mailbox_id = os.getenv("OUTREACH_MAILBOX_ID")
# Validate required environment variables
if not all([zoom_api_token, surfe_api_key, outreach_access_token, webinar_id, sequence_id, mailbox_id]):
print("Error: Missing required environment variables. Please check your .env file.")
return
try:
# Initialize services
zoom_service = ZoomService(zoom_api_token)
surfe_service = SurfeService(surfe_api_key, version="v2")
outreach_service = OutreachService(outreach_access_token)
# Step 1: Fetch webinar registrants from Zoom
print(f"Fetching registrants for webinar {webinar_id}...")
registrants = zoom_service.get_all_webinar_registrants(webinar_id)
print(f"Found {len(registrants)} registrants for the webinar")
# Step 2: Process registrants to extract essential information
print("Processing registrants to extract name and company information...")
processed_registrants = zoom_service.process_webinar_registrants(registrants)
print(f"Found {len(processed_registrants)} registrants with valid name and company information")
if not processed_registrants:
print("No registrants with valid information found. Exiting...")
return
# Step 3: Prepare and start Surfe enrichment
print("Preparing Surfe enrichment...")
surfe_payload = surfe_service.prepare_people_payload(
processed_registrants,
list_name=f"Zoom Webinar {webinar_id} Enrichment {time.strftime('%Y-%m-%d %H:%M:%S')}"
)
print("Starting Surfe enrichment process...")
enrichment_id = surfe_service.start_enrichment(surfe_payload)
print(f"Enrichment started with ID: {enrichment_id}")
# Step 4: Poll for enrichment results
print("Polling for enrichment results...")
enriched_data = surfe_service.poll_enrichment_status(enrichment_id)
print("Enrichment completed successfully")
enriched_people = enriched_data.get("people", [])
print(f"Enriched {len(enriched_people)} registrants")
# Step 5: Create prospects in Outreach and add to sequence
print("Adding enriched registrants to Outreach...")
event_name = zoom_service.get_webinar_details(webinar_id).get("topic")
success_count = 0
for person in enriched_people:
try:
# Format the enriched data for Outreach
prospect_data = outreach_service.format_enriched_data_for_outreach(person, event_name)
# Check if we have a valid email
if not prospect_data.get("emails"):
print(f"Skipping {person.get('firstName')} {person.get('lastName')} - No valid email found")
continue
# Check if prospect already exists in Outreach
existing_prospect = outreach_service.find_prospect_by_email(prospect_data["emails"])
if existing_prospect:
prospect_id = existing_prospect["id"]
print(f"Prospect {person.get('firstName')} {person.get('lastName')} already exists with ID: {prospect_id}")
else:
# Create a new prospect
print(f"Creating prospect for {person.get('firstName')} {person.get('lastName')}...")
prospect_response = outreach_service.create_prospect(prospect_data)
prospect_id = prospect_response["data"]["id"]
print(f"Created prospect with ID: {prospect_id}")
# Add the prospect to the sequence
print(f"Adding prospect {prospect_id} to sequence {sequence_id}...")
outreach_service.add_prospect_to_sequence(prospect_id, sequence_id, mailbox_id)
print(f"Successfully added prospect to sequence")
success_count += 1
except Exception as e:
print(f"Error processing {person.get('firstName')} {person.get('lastName')}: {str(e)}")
print(f"Successfully added {success_count} prospects to Outreach sequence {sequence_id}")
except Exception as e:
print(f"Error: {str(e)}")
if __name__ == "__main__":
main()
Final Notes: Credits, Quotas, and Rate Limiting
Credits & Quotas
Surfe’s API uses a credit system for people enrichment. Retrieving email, landline, and job details consumes email credits, while retrieving mobile phone numbers consumes mobile credits. There are also daily quotas, such as 2,000 people enrichments per day and 200 organization look-alike searches per day. For more information on credits and quotas, please speak to a Surfe representative to discuss a tailored plan that works for you and your business needs. Quotas reset at midnight (local time), and additional credits can be purchased if needed. For full details, refer to the Credits & Quotas documentation.
Rate Limiting
Surfe enforces rate limits to ensure fair API usage. Users can make up to 10 requests per second, with short bursts of up to 20 requests allowed. The limit resets every minute. Exceeding this results in a 429 Too Many Requests error, so it’s recommended to implement retries in case of rate limit issues. Learn more in the Rate Limits documentation.

Webinar’s done. Leads are in. Let’s make them count.
Go on—give Surfe a spin and turn those Zoom leads into actual pipeline.
Contact Enrichment API FAQs
What’s the Benefit of Enriching Zoom Webinar Leads?
Webinar registrants are warm leads, but they’re often missing key details like email validation, phone numbers, or job titles. Enriching this data means your sales team can follow up faster, more accurately, and more personally—without wasting time on research or bad contact info.
How Does This Script Help with Webinar Follow-Up?
This script fetches webinar registrants directly from Zoom, enriches them using Surfe’s API, and pushes them into Outreach as fully-formed prospects. That means no CSV exports, manual copy-pasting, or delayed follow-ups—it’s one streamlined flow from signup to sequence.
Do I Need API Access for Zoom and Outreach?
Yes. You’ll need access to the Zoom API (via JWT or OAuth) and Outreach’s API (via OAuth token). Setting this up usually takes just a few minutes through their developer portals. We walk you through everything you need in the tutorial.
Can I Run This Script Without a Developer?
If you’re comfortable with basic Python or know how to follow setup instructions, you can absolutely run this on your own. The tutorial is written for non-devs and includes environment setup, API key loading, and all required packages.
What Kind of Data Does Surfe’s API Enrich for Webinar Leads?
Surfe can return verified email addresses, mobile numbers, job titles, seniority, LinkedIn URLs, and more. The better your input data (e.g., name + company), the more complete and accurate your enrichment results will be.
What Happens If a Prospect Already Exists in Outreach?
The script includes a check to avoid creating duplicates. If a registrant’s email already exists in Outreach, it simply skips creation and moves straight to sequence assignment—saving time and keeping your CRM clean.
How Often Should I Run the Script?
That’s up to your workflow. You can run it after every webinar, on a weekly schedule, or as soon as new registrants appear. Just make sure to monitor your API credit usage if you’re processing large lists frequently.