# 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:ticket` or `zoom_events:write:ticket:admin` scope. - **CRM xxport** CSV or JSON export from your CRM with attendee data including `email`, `first_name`, and `last_name`. --- ## What you will learn 1. How to retrieve event ticket types from the Webinars Plus & Events API. 2. How to prepare bulk attendee data from CRM exports. 3. How to batch-process 500+ registrations (30 at a time per API limit). 4. How to handle partial failures and retry logic. 5. 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 ```bash curl -X GET "https://api.zoom.us/v2/zoom_events/events/kNqLPC6hSFiZ9NpgjA549w/ticket_types" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` ### Python ```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) ```javascript 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 ```json { "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 ```csv 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 ```python 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 ```javascript 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 ```json [ { "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 ```python 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 ```javascript 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 ```python 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 ```javascript 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 ```shell 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 ```python 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 ```javascript 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 ```python 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 ```javascript 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 ```ini 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: 1. **Navigate to your event** → People → Attendees. 2. **Check total registrations:** Should show ~500 attendees. 3. **Verify attendee details:** Check a few records match your CRM data. 4. **Confirm email delivery:** Check "Communications" → "Emails" for send stats. **API verification (optional)** ```python 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: ```bash 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: ```python 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: 1. Remove unconfigured fields from your payload. 2. 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: ```python # 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: ```python 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 1. **Custom email integration:** Set `send_notification: false` and send branded emails from your marketing platform. 2. **Real-time sync:** Build a webhook listener to sync Salesforce with Zoom Webinars Plus & Events automatically. 3. **Session-specific registration:** For recurring events, add `session_id` to register attendees for specific dates. 4. **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: ```python #!/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** ```bash 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:ticket` or `zoom_events:write:ticket:admin` - `zoom_events:read:list_ticket_types` for 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