Load data
What you will build
An automated workflow that imports 500 VIP customers from your Salesforce CRM and pre-registers them for your annual virtual multi-session event, complete with custom join links and automated email confirmations.
Time required
30—45 minutes
Use case
You are hosting a high-value virtual multi-session event and need to automatically register your top 500 customers from Salesforce. Rather than manually entering each attendee or asking them to fill out a registration form, you will use the Webinars Plus & Events API to programmatically create tickets (pre-register attendees) and send them personalized join links.
Prerequisites
Before you start, ensure you have:
- Published event Your event must be published (not draft) - attempting to pre-register for draft events returns error 26501.
- Ticket Type created At least one ticket type configured during event setup.
- API credentials OAuth access token with
zoom_events:write:ticketorzoom_events:write:ticket:adminscope. - CRM xxport CSV or JSON export from your CRM with attendee data including
email,first_name, andlast_name.
What you will learn
- How to retrieve event ticket types from the Webinars Plus & Events API.
- How to prepare bulk attendee data from CRM exports.
- How to batch-process 500+ registrations (30 at a time per API limit).
- How to handle partial failures and retry logic.
- How to verify successful registrations and extract join URLs.
Step 1: Retrieve your event's Ticket Type ID
Every attendee needs to be assigned a ticket type (e.g., "VIP Pass", "General Admission"). First, fetch the available ticket types for your event. If you do not provide a ticket type ID when registering an attendee, the system will use the event's default ticket type, when available.
cURL
curl -X GET "https://api.zoom.us/v2/zoom_events/events/kNqLPC6hSFiZ9NpgjA549w/ticket_types" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Python
import requests
event_id = "kNqLPC6hSFiZ9NpgjA549w"
access_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
response = requests.get(
f"https://api.zoom.us/v2/zoom_events/events/{event_id}/ticket_types",
headers={"Authorization": f"Bearer {access_token}"}
)
response.raise_for_status() # Check for HTTP errors
ticket_types = response.json()
print(f"Found {ticket_types['total_records']} ticket type(s)")
JavaScript (Node.js)
const axios = require("axios");
const eventId = "kNqLPC6hSFiZ9NpgjA549w";
const accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
async function getTicketTypes() {
try {
const response = await axios.get(
`https://api.zoom.us/v2/zoom_events/events/${eventId}/ticket_types`,
{
headers: { Authorization: `Bearer ${accessToken}` },
},
);
console.log(`Found ${response.data.total_records} ticket type(s)`);
return response.data.ticket_types;
} catch (error) {
console.error(
"Failed to get ticket types:",
error.response?.data || error.message,
);
throw error;
}
}
Expected response
{
"total_records": 2,
"ticket_types": [
{
"ticket_type_id": "1",
"name": "VIP Pass",
"price": 0,
"quantity": 500,
"description": "Exclusive access for VIP customers"
},
{
"ticket_type_id": "2",
"name": "General Admission",
"price": 0,
"quantity": 1000
}
]
}
Save the ticket_type_id - in this example, we'll use "1" (VIP Pass) for our 500 customers.
Step 2: Prepare your CRM data
Export your 500 VIP customers from Salesforce and format them for the Webinars Plus & Events API.
Example CSV export from Salesforce
ContactId,Email,FirstName,LastName,Company,Title
003xx000001ABC1,john.smith@acme.com,John,Smith,Acme Corp,CTO
003xx000001ABC2,sarah.jones@techco.com,Sarah,Jones,TechCo Inc,VP Engineering
003xx000001ABC3,michael.brown@startup.io,Michael,Brown,Startup.io,Founder
003xx000001ABC4,emily.davis@enterprise.com,Emily,Davis,Enterprise LLC,Director
Transform to API-ready format
Convert your CSV into the format required by the API.
Python Script
import csv
import json
def load_crm_data(csv_file):
"""Load CRM data and transform to API format"""
attendees = []
with open(csv_file, 'r') as file:
reader = csv.DictReader(file)
for row in reader:
attendees.append({
"email": row['Email'],
"first_name": row['FirstName'],
"last_name": row['LastName'],
"ticket_type_id": "1", # VIP Pass
"send_notification": True, # Send Zoom confirmation email
"external_ticket_id": row['ContactId'], # Track back to Salesforce
"custom_questions": [
{
"title": "custom_q1", # If you have custom questions
"answer": row['Company']
}
]
})
return attendees
# Load data
vip_customers = load_crm_data('salesforce_export.csv')
print(f"Loaded {len(vip_customers)} VIP customers for registration")
JavaScript Script
const fs = require("fs");
const csv = require("csv-parser");
async function loadCrmData(csvFile) {
const attendees = [];
return new Promise((resolve, reject) => {
fs.createReadStream(csvFile)
.pipe(csv())
.on("data", (row) => {
attendees.push({
email: row.Email,
first_name: row.FirstName,
last_name: row.LastName,
ticket_type_id: "1", // VIP Pass
send_notification: true,
external_ticket_id: row.ContactId,
custom_questions: [
{
title: "custom_q1",
answer: row.Company,
},
],
});
})
.on("end", () => {
console.log(`Loaded ${attendees.length} VIP customers`);
resolve(attendees);
})
.on("error", reject);
});
}
Sample Transformed Data
[
{
"email": "john.smith@acme.com",
"first_name": "John",
"last_name": "Smith",
"ticket_type_id": "1",
"send_notification": true,
"external_ticket_id": "003xx000001ABC1",
"custom_questions": [{ "title": "custom_q1", "answer": "Acme Corp" }]
},
{
"email": "sarah.jones@techco.com",
"first_name": "Sarah",
"last_name": "Jones",
"ticket_type_id": "1",
"send_notification": true,
"external_ticket_id": "003xx000001ABC2",
"custom_questions": [{ "title": "custom_q1", "answer": "TechCo Inc" }]
}
]
Step 3: Validate data before upload
Critical Step: Validate your data to avoid API errors during bulk upload.
Python Validation Script
def validate_attendees(attendees):
"""Validate attendee data before API submission"""
errors = []
seen_emails = set()
for i, attendee in enumerate(attendees):
# Check required fields
if not attendee.get('email') or '@' not in attendee['email']:
errors.append(f"Row {i+1}: Invalid email '{attendee.get('email')}'")
if not attendee.get('first_name'):
errors.append(f"Row {i+1}: Missing first_name")
if not attendee.get('last_name'):
errors.append(f"Row {i+1}: Missing last_name")
# Check for duplicate emails
email_lower = attendee.get('email', '').lower()
if email_lower in seen_emails:
errors.append(f"Row {i+1}: Duplicate email '{attendee['email']}'")
seen_emails.add(email_lower)
return errors
# Validate before processing
validation_errors = validate_attendees(vip_customers)
if validation_errors:
print("Validation failed:")
for error in validation_errors[:10]: # Show first 10 errors
print(f" - {error}")
exit(1)
else:
print(" Validation passed - ready to upload")
JavaScript validation script
function validateAttendees(attendees) {
const errors = [];
const seenEmails = new Set();
attendees.forEach((attendee, i) => {
// Check required fields
if (!attendee.email || !attendee.email.includes("@")) {
errors.push(`Row ${i + 1}: Invalid email '${attendee.email}'`);
}
if (!attendee.first_name) {
errors.push(`Row ${i + 1}: Missing first_name`);
}
if (!attendee.last_name) {
errors.push(`Row ${i + 1}: Missing last_name`);
}
// Check for duplicates
const emailLower = (attendee.email || "").toLowerCase();
if (seenEmails.has(emailLower)) {
errors.push(`Row ${i + 1}: Duplicate email '${attendee.email}'`);
}
seenEmails.add(emailLower);
});
return errors;
}
// Validate
const validationErrors = validateAttendees(vipCustomers);
if (validationErrors.length > 0) {
console.error("Validation failed:");
validationErrors.slice(0, 10).forEach((err) => console.error(` - ${err}`));
process.exit(1);
} else {
console.log(" Validation passed - ready to upload");
}
Step 4: Batch Upload 500 Attendees (30 per Request)
The Webinars Plus & Events API allows up to 30 tickets per request. To register 500 attendees, you will make 17 API calls (16 batches of 30 + 1 batch of 20).
Python Implementation with Progress Tracking
import requests
import time
from math import ceil
def create_tickets_batch(event_id, access_token, tickets):
"""Create a batch of tickets via API"""
url = f"https://api.zoom.us/v2/zoom_events/events/{event_id}/tickets"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, json={"tickets": tickets})
response.raise_for_status() # Check for HTTP errors
return response.json()
def bulk_register_attendees(event_id, access_token, attendees, batch_size=30):
"""Register attendees in batches with progress tracking"""
total = len(attendees)
num_batches = ceil(total / batch_size)
all_tickets = []
all_errors = []
print(f"Starting bulk registration: {total} attendees in {num_batches} batches\n")
for batch_num in range(num_batches):
start_idx = batch_num * batch_size
end_idx = min(start_idx + batch_size, total)
batch = attendees[start_idx:end_idx]
print(f"Batch {batch_num + 1}/{num_batches}: Processing {len(batch)} attendees...")
try:
result = create_tickets_batch(event_id, access_token, batch)
# Track successful tickets
if 'tickets' in result:
all_tickets.extend(result['tickets'])
print(f" Successfully registered {len(result['tickets'])} attendees")
# Track errors
if 'errors' in result and result['errors']:
all_errors.extend(result['errors'])
print(f" {len(result['errors'])} failures")
# Rate limiting: wait 1 second between batches
if batch_num < num_batches - 1:
time.sleep(1)
except Exception as e:
print(f" Batch failed with error: {str(e)}")
all_errors.extend([{"email": a["email"], "error": str(e)} for a in batch])
print(f"\n{'='*60}")
print(f"Bulk Registration Complete")
print(f"{'='*60}")
print(f"Total Processed: {total}")
print(f"Successful: {len(all_tickets)}")
print(f"Failed: {len(all_errors)}")
print(f"Success Rate: {len(all_tickets)/total*100:.1f}%")
return all_tickets, all_errors
# Execute bulk registration
event_id = "kNqLPC6hSFiZ9NpgjA549w"
access_token = "your_access_token_here"
successful_tickets, failed_tickets = bulk_register_attendees(
event_id,
access_token,
vip_customers
)
JavaScript Implementation with Progress Tracking
const axios = require("axios");
async function createTicketsBatch(eventId, accessToken, tickets) {
try {
const response = await axios.post(
`https://api.zoom.us/v2/zoom_events/events/${eventId}/tickets`,
{ tickets },
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
},
);
return response.data;
} catch (error) {
console.error(
"Failed to create tickets batch:",
error.response?.data || error.message,
);
throw error;
}
}
async function bulkRegisterAttendees(
eventId,
accessToken,
attendees,
batchSize = 30,
) {
const total = attendees.length;
const numBatches = Math.ceil(total / batchSize);
const allTickets = [];
const allErrors = [];
console.log(
`Starting bulk registration: ${total} attendees in ${numBatches} batches\n`,
);
for (let batchNum = 0; batchNum < numBatches; batchNum++) {
const startIdx = batchNum * batchSize;
const endIdx = Math.min(startIdx + batchSize, total);
const batch = attendees.slice(startIdx, endIdx);
console.log(
`Batch ${batchNum + 1}/${numBatches}: Processing ${batch.length} attendees...`,
);
try {
const result = await createTicketsBatch(
eventId,
accessToken,
batch,
);
// Track successful tickets
if (result.tickets) {
allTickets.push(...result.tickets);
console.log(
` Successfully registered ${result.tickets.length} attendees`,
);
}
// Track errors
if (result.errors && result.errors.length > 0) {
allErrors.push(...result.errors);
console.log(` ✗ ${result.errors.length} failures`);
}
// Rate limiting: wait 1 second between batches
if (batchNum < numBatches - 1) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
} catch (error) {
const errorMsg =
error.response?.data || error.message || "Unknown error";
console.log(` ✗ Batch failed with error: ${errorMsg}`);
allErrors.push(
...batch.map((a) => ({ email: a.email, error: errorMsg })),
);
}
}
console.log(`\n${"=".repeat(60)}`);
console.log("Bulk Registration Complete");
console.log("=".repeat(60));
console.log(`Total Processed: ${total}`);
console.log(`Successful: ${allTickets.length}`);
console.log(`Failed: ${allErrors.length}`);
console.log(
`Success Rate: ${((allTickets.length / total) * 100).toFixed(1)}%`,
);
return { tickets: allTickets, errors: allErrors };
}
// Execute
const eventId = "kNqLPC6hSFiZ9NpgjA549w";
const accessToken = "your_access_token_here";
(async () => {
const result = await bulkRegisterAttendees(
eventId,
accessToken,
vipCustomers,
);
console.log(`\nFirst 3 registered attendees:`);
result.tickets.slice(0, 3).forEach((t) => {
console.log(` ${t.email}: ${t.event_join_link || "N/A"}`);
});
})();
Expected Console Output
Starting bulk registration: 500 attendees in 17 batches
Batch 1/17: Processing 30 attendees...
Successfully registered 30 attendees
Batch 2/17: Processing 30 attendees...
Successfully registered 30 attendees
Batch 3/17: Processing 30 attendees...
Successfully registered 29 attendees
1 failures
...
Batch 17/17: Processing 20 attendees...
Successfully registered 20 attendees
============================================================
Bulk Registration Complete
============================================================
Total Processed: 500
Successful: 497
Failed: 3
Success Rate: 99.4%
Step 5: Handle Failures and Retry
Some registrations may fail (duplicate emails, invalid data, rate limits). Implement retry logic for failed attendees.
Python Retry Logic
import time
def retry_failed_registrations(event_id, access_token, failed_attendees, max_retries=3):
"""Retry failed registrations with exponential backoff"""
retry_queue = failed_attendees.copy()
final_failures = []
for attempt in range(1, max_retries + 1):
if not retry_queue:
break
print(f"\nRetry Attempt {attempt}/{max_retries}: {len(retry_queue)} attendees")
successful = []
still_failing = []
for attendee in retry_queue:
try:
# Retry individually for better error isolation
result = create_tickets_batch(event_id, access_token, [attendee])
if result.get('tickets'):
successful.append(result['tickets'][0])
print(f" Retry success: {attendee['email']}")
else:
still_failing.append(attendee)
print(f" Still failing: {attendee['email']} - {result.get('errors', [{}])[0].get('message', 'Unknown error')}")
time.sleep(0.5) # Rate limit protection
except Exception as e:
still_failing.append(attendee)
print(f" Error: {attendee['email']} - {str(e)}")
retry_queue = still_failing
# Exponential backoff between retry attempts
if retry_queue and attempt < max_retries:
backoff = 2 ** attempt
print(f"Waiting {backoff} seconds before next retry...")
time.sleep(backoff)
print(f"\nRetry Summary:")
print(f" Recovered: {len(successful)}")
print(f" Permanent Failures: {len(retry_queue)}")
return successful, retry_queue
# Retry failed attendees from initial upload
if failed_tickets:
print(f"\nRetrying {len(failed_tickets)} failed registrations...")
recovered, permanent_failures = retry_failed_registrations(
event_id,
access_token,
[a for a in vip_customers if a['email'] in [f.get('email') for f in failed_tickets]]
)
successful_tickets.extend(recovered)
if permanent_failures:
print(f"\n⚠️ {len(permanent_failures)} attendees could not be registered:")
for failure in permanent_failures[:5]:
print(f" - {failure['email']}")
JavaScript Retry Logic
async function retryFailedRegistrations(
eventId,
accessToken,
failedAttendees,
maxRetries = 3,
) {
let retryQueue = [...failedAttendees];
const successful = [];
for (let attempt = 1; attempt <= maxRetries; attempt++) {
if (retryQueue.length === 0) break;
console.log(
`\nRetry Attempt ${attempt}/${maxRetries}: ${retryQueue.length} attendees`,
);
const stillFailing = [];
for (const attendee of retryQueue) {
try {
const result = await createTicketsBatch(eventId, accessToken, [
attendee,
]);
if (result.tickets && result.tickets.length > 0) {
successful.push(result.tickets[0]);
console.log(` Retry success: ${attendee.email}`);
} else {
stillFailing.push(attendee);
const errorMsg =
result.errors?.[0]?.message || "Unknown error";
console.log(
` Still failing: ${attendee.email} - ${errorMsg}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 500));
} catch (error) {
stillFailing.push(attendee);
const errorMsg =
error.response?.data || error.message || "Unknown error";
console.log(` Error: ${attendee.email} - ${errorMsg}`);
}
}
retryQueue = stillFailing;
if (retryQueue.length > 0 && attempt < maxRetries) {
const backoff = Math.pow(2, attempt);
console.log(`Waiting ${backoff} seconds before next retry...`);
await new Promise((resolve) => setTimeout(resolve, backoff * 1000));
}
}
console.log(`\nRetry Summary:`);
console.log(` Recovered: ${successful.length}`);
console.log(` Permanent Failures: ${retryQueue.length}`);
return { recovered: successful, failures: retryQueue };
}
Step 6: Verify Registrations and Extract Join URLs
After bulk registration, verify the results and extract join URLs for your records.
Python Verification Script
def save_registration_results(tickets, output_file='registration_results.csv'):
"""Save successful registrations to CSV"""
import csv
with open(output_file, 'w', newline='') as file:
writer = csv.DictWriter(file, fieldnames=[
'ticket_id', 'email', 'first_name', 'last_name',
'event_join_link', 'external_ticket_id', 'status'
])
writer.writeheader()
for ticket in tickets:
writer.writerow({
'ticket_id': ticket.get('ticket_id'),
'email': ticket.get('email'),
'first_name': ticket.get('first_name'),
'last_name': ticket.get('last_name'),
'event_join_link': ticket.get('event_join_link'),
'external_ticket_id': ticket.get('external_ticket_id'),
'status': ticket.get('status', 'active')
})
print(f"\n✓ Registration results saved to {output_file}")
# Save results
save_registration_results(successful_tickets)
# Show sample results
print("\nSample Join URLs (first 5):")
for ticket in successful_tickets[:5]:
print(f" {ticket['email']}: {ticket.get('event_join_link', 'N/A')}")
JavaScript Verification Script
const fs = require("fs");
function saveRegistrationResults(
tickets,
outputFile = "registration_results.csv",
) {
const csvHeader =
"ticket_id,email,first_name,last_name,event_join_link,external_ticket_id,status\n";
const csvRows = tickets
.map((ticket) => {
return [
ticket.ticket_id,
ticket.email,
ticket.first_name,
ticket.last_name,
ticket.event_join_link || "",
ticket.external_ticket_id || "",
ticket.status || "active",
].join(",");
})
.join("\n");
fs.writeFileSync(outputFile, csvHeader + csvRows);
console.log(`\n Registration results saved to ${outputFile}`);
}
// Save results
saveRegistrationResults(result.tickets);
// Show sample results
console.log("\nSample Join URLs (first 5):");
result.tickets.slice(0, 5).forEach((ticket) => {
console.log(` ${ticket.email}: ${ticket.event_join_link || "N/A"}`);
});
Sample Output: registration_results.csv
ticket_id,email,first_name,last_name,event_join_link,external_ticket_id,status
abc123def456,john.smith@acme.com,John,Smith,https://events.zoom.us/j/abc123def456,003xx000001ABC1,active
xyz789uvw012,sarah.jones@techco.com,Sarah,Jones,https://events.zoom.us/j/xyz789uvw012,003xx000001ABC2,active
mno345pqr678,michael.brown@startup.io,Michael,Brown,https://events.zoom.us/j/mno345pqr678,003xx000001ABC3,active
Step 7: Verify in Webinars Plus & Events Dashboard
Log into your Webinars Plus & Events account and verify the registrations:
- Navigate to your event → People → Attendees.
- Check total registrations: Should show ~500 attendees.
- Verify attendee details: Check a few records match your CRM data.
- Confirm email delivery: Check "Communications" → "Emails" for send stats.
API verification (optional)
def verify_registrations(event_id, access_token):
"""Verify total registration count via API"""
url = f"https://api.zoom.us/v2/zoom_events/events/{event_id}/tickets"
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(url, headers=headers)
response.raise_for_status() # Check for HTTP errors
data = response.json()
print(f"\nAPI Verification:")
print(f" Total Registrations: {data.get('total_records', 0)}")
print(f" Active Tickets: {len([t for t in data.get('tickets', [])])}")
verify_registrations(event_id, access_token)
Common Error scenarios and solutions
Error 26501: "Event is not published yet"
Cause
Attempting to pre-register attendees for a draft event.
Solution
Publish your event first:
curl -X POST "https://api.zoom.us/v2/zoom_events/events/{eventId}/event_actions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-d '{"operation": "publish"}'
Error 26502 or 2007: "Invalid ticket_type_id"
Cause
The ticket type ID doesn't exist for this event.
Solution
Re-fetch ticket types and verify the ID:
response = requests.get(f"https://api.zoom.us/v2/zoom_events/events/{event_id}/ticket_types")
response.raise_for_status() # Check for HTTP errors
print(response.json())
Error 260503: "Non configured fields"
Cause
Using validation_level=strict and submitting fields not configured for the event.
Solution
Either:
- Remove unconfigured fields from your payload.
- Use
validation_level=standard(default) which ignores extra fields.
Duplicate email error
Cause
Attempting to register the same email twice.
Solution
Check for existing registrations before bulk upload:
# Get existing tickets
response = requests.get(f"https://api.zoom.us/v2/zoom_events/events/{event_id}/tickets")
response.raise_for_status() # Check for HTTP errors
existing = response.json()
existing_emails = {t['email'].lower() for t in existing.get('tickets', [])}
# Filter out duplicates
new_attendees = [a for a in vip_customers if a['email'].lower() not in existing_emails]
HTTP 429: Rate limit exceeded
Cause
Too many requests in a short period.
Solution
Implement exponential backoff:
import time
def api_call_with_retry(func, *args, max_retries=5):
for attempt in range(max_retries):
try:
return func(*args)
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
wait_time = 2 ** attempt
print(f"Rate limited. Waiting {wait_time}s...")
time.sleep(wait_time)
else:
raise
raise Exception("Max retries exceeded")
What you built
Congratulations! You have built a complete bulk registration system that:
- Retrieves ticket types from your Webinars Plus & Events event.
- Loads and validates 500 VIP customers from Salesforce.
- Batch-processes registrations in chunks of 30 (API limit compliance).
- Handles failures gracefully with retry logic and error tracking.
- Extracts join URLs for each attendee.
- Exports results to CSV for record-keeping.
- Tracks back to CRM using external_ticket_id for future reference.
Key outcomes
- Time saved: ~20 hours of manual data entry eliminated
- Accuracy: 99%+ success rate with automated validation
- Attendee experience: Immediate email confirmations with personalized join links
- CRM integration: Bidirectional tracking with external_ticket_id
- Scalability: Can handle 1,000+ attendees with same workflow
Next steps
Enhance your workflow
- Custom email integration: Set
send_notification: falseand send branded emails from your marketing platform. - Real-time sync: Build a webhook listener to sync Salesforce with Zoom Webinars Plus & Events automatically.
- Session-specific registration: For recurring events, add
session_idto register attendees for specific dates. - Custom questions: Capture additional data by configuring event questions and including answers in the payload.
Complete code example
Here is a production-ready Python script combining all steps:
#!/usr/bin/env python3
"""
Bulk Pre-Register Attendees from CSV to Zoom Webinars Plus & Events
Usage: python bulk_register.py --event-id EVENT_ID --csv salesforce_export.csv
"""
import argparse
import csv
import requests
import time
from math import ceil
class ZoomEventsBulkRegistration:
def __init__(self, event_id, access_token):
self.event_id = event_id
self.access_token = access_token
self.base_url = "https://api.zoom.us/v2/zoom_events/events"
def get_ticket_type_id(self):
"""Fetch the first available ticket type"""
response = requests.get(
f"{self.base_url}/{self.event_id}/ticket_types",
headers={"Authorization": f"Bearer {self.access_token}"}
)
response.raise_for_status()
ticket_types = response.json().get('ticket_types', [])
if not ticket_types:
raise ValueError("No ticket types found for this event")
return ticket_types[0]['ticket_type_id']
def load_csv(self, csv_file, ticket_type_id):
"""Load attendees from CSV"""
attendees = []
with open(csv_file, 'r') as file:
reader = csv.DictReader(file)
for row in reader:
attendees.append({
"email": row['Email'],
"first_name": row['FirstName'],
"last_name": row['LastName'],
"ticket_type_id": ticket_type_id,
"send_notification": True,
"external_ticket_id": row.get('ContactId', '')
})
return attendees
def validate_attendees(self, attendees):
"""Validate attendee data"""
errors = []
seen = set()
for i, a in enumerate(attendees):
if not a.get('email') or '@' not in a['email']:
errors.append(f"Row {i+1}: Invalid email")
if not a.get('first_name') or not a.get('last_name'):
errors.append(f"Row {i+1}: Missing name")
email_lower = a['email'].lower()
if email_lower in seen:
errors.append(f"Row {i+1}: Duplicate email {a['email']}")
seen.add(email_lower)
return errors
def create_tickets_batch(self, tickets):
"""Create batch of tickets"""
response = requests.post(
f"{self.base_url}/{self.event_id}/tickets",
headers={
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json"
},
json={"tickets": tickets}
)
response.raise_for_status()
return response.json()
def bulk_register(self, attendees, batch_size=30):
"""Register attendees in batches"""
total = len(attendees)
num_batches = ceil(total / batch_size)
all_tickets = []
all_errors = []
print(f"Registering {total} attendees in {num_batches} batches\n")
for i in range(num_batches):
start = i * batch_size
end = min(start + batch_size, total)
batch = attendees[start:end]
print(f"Batch {i+1}/{num_batches}: {len(batch)} attendees...", end=' ')
try:
result = self.create_tickets_batch(batch)
all_tickets.extend(result.get('tickets', []))
all_errors.extend(result.get('errors', []))
print(f" {len(result.get('tickets', []))} succeeded")
time.sleep(1) # Rate limiting
except Exception as e:
print(f" Failed: {e}")
all_errors.extend(batch)
print(f"\nComplete: {len(all_tickets)} succeeded, {len(all_errors)} failed")
return all_tickets, all_errors
def save_results(self, tickets, filename='results.csv'):
"""Save results to CSV"""
with open(filename, 'w', newline='') as file:
writer = csv.DictWriter(file, fieldnames=[
'ticket_id', 'email', 'first_name', 'last_name', 'event_join_link', 'status'
])
writer.writeheader()
for ticket in tickets:
writer.writerow({
'ticket_id': ticket.get('ticket_id'),
'email': ticket.get('email'),
'first_name': ticket.get('first_name'),
'last_name': ticket.get('last_name'),
'event_join_link': ticket.get('event_join_link', ''),
'status': ticket.get('status', 'active')
})
print(f" Results saved to {filename}")
def main():
parser = argparse.ArgumentParser(description='Bulk register attendees')
parser.add_argument('--event-id', required=True, help='Zoom Events event ID')
parser.add_argument('--csv', required=True, help='CSV file with attendees')
parser.add_argument('--token', required=True, help='OAuth access token')
args = parser.parse_args()
# Initialize
registrar = ZoomEventsBulkRegistration(args.event_id, args.token)
# Get ticket type
ticket_type_id = registrar.get_ticket_type_id()
print(f"Using ticket type ID: {ticket_type_id}\n")
# Load CSV
attendees = registrar.load_csv(args.csv, ticket_type_id)
print(f"Loaded {len(attendees)} attendees from CSV")
# Validate
errors = registrar.validate_attendees(attendees)
if errors:
print("Validation failed:")
for error in errors[:10]:
print(f" {error}")
return
print(" Validation passed\n")
# Register
tickets, failures = registrar.bulk_register(attendees)
# Save results
if tickets:
registrar.save_results(tickets)
if __name__ == '__main__':
main()
Run the Script
python bulk_register.py \
--event-id kNqLPC6hSFiZ9NpgjA549w \
--csv salesforce_export.csv \
--token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
API reference summary
| Endpoint | Method | Purpose | Rate Limit |
|---|---|---|---|
/zoom_events/events/{eventId}/ticket_types | GET | List ticket types | Standard |
/zoom_events/events/{eventId}/tickets | POST | Create tickets (max 30/request) | Standard |
/zoom_events/events/{eventId}/tickets | GET | List all tickets | Standard |
/zoom_events/events/{eventId}/tickets/{ticketId} | GET | Get ticket details | Standard |
/zoom_events/events/{eventId}/tickets/{ticketId} | PATCH | Update ticket | Standard |
/zoom_events/events/{eventId}/tickets/{ticketId} | DELETE | Delete ticket | Standard |
Required scopes
zoom_events:write:ticketorzoom_events:write:ticket:adminzoom_events:read:list_ticket_typesfor listing ticket types
Frequently asked questions
Can I register more than 30 attendees at once?
No, the API limit is 30 tickets per request. Use batching (as shown in this tutorial).
Results if some attendees in a batch fail?
The API processes all 30, returns successful tickets in response.tickets and failures in response.errors. Partial success is normal.
Can I update a ticket after creation?
Yes, use PATCH /zoom_events/events/{eventId}/tickets/{ticketId} to update ticket details.
How do I delete a registration?
Use DELETE /zoom_events/events/{eventId}/tickets/{ticketId}. The attendee will receive a cancellation email.
Can I disable Zoom's confirmation emails?
Yes, set send_notification: false in the ticket payload. You are then responsible for sending custom emails with join URLs.
Do join URLs expire?
No, join URLs remain valid until the ticket is deleted or the event ends.
Can I register attendees for recurring events?
Yes, include session_ids in the payload to register for specific session(s).
How do I handle international characters (é, ñ, 中)?
The API accepts UTF-8. Ensure your CSV is UTF-8 encoded.
Troubleshooting checklist
- Event is published (not draft)
- Access token has correct scopes (
zoom_events:write:ticket) - Ticket type ID is valid for this event
- No duplicate emails in upload
- Batch size ≤ 30 attendees
- Rate limiting implemented (1s between batches)
- Error handling catches HTTP 429 (rate limit)
- Validation runs before upload