Eventbrite Integration
Gordon CRM integrates with Eventbrite (opens in a new tab) to automatically sync events, registrations, and attendee data into your CRM workspace. The integration uses OAuth 2.0 for authentication and webhooks for real-time data synchronization.
Connection Setup
OAuth Flow
The Eventbrite integration uses the standard OAuth 2.0 authorization code flow:
Dashboard → Settings → Integrations → "Connect Eventbrite"
→ Redirect to Eventbrite authorization page
→ User grants access
→ Eventbrite redirects to /api/auth/eventbrite/callback
→ Exchange code for access token
→ Fetch organization info
→ Create webhook subscription
→ Store connection in eventbrite_connections tableThe callback handler performs five steps:
- Token Exchange — Exchanges the authorization code for a long-lived access token.
- Organization Discovery — Fetches the user's Eventbrite organization ID and name.
- Webhook Registration — Creates a webhook subscription on the Eventbrite organization to receive
order.placed,order.refunded, andattendee.updatedevents. - Connection Storage — Persists the access token, organization info, and webhook ID in the
eventbrite_connectionstable. - Redirect — Returns the user to Settings → Integrations with a success indicator.
Connection Model
Each workspace can have one Eventbrite connection, enforced by a unique constraint on workspace_id:
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
workspace_id | uuid | FK → workspaces, unique |
access_token | text | Eventbrite OAuth access token |
organization_id | text | Eventbrite organization ID |
organization_name | text | Human-readable org name for display |
webhook_id | text | Eventbrite webhook subscription ID |
auto_subscribe_attendees | boolean | When true, webhook-created contacts are auto-subscribed to marketing. Default false. |
connected_at | timestamptz | When the connection was established |
Disconnecting
Disconnecting Eventbrite from a workspace:
- Deletes the webhook subscription on Eventbrite's side (best-effort — non-critical if it fails).
- Removes the
eventbrite_connectionsrow.
Note: Disconnecting does not delete any previously imported events or registrations. Those records remain in the CRM as historical data.
RBAC
- Owner & Admin — Can connect and disconnect Eventbrite.
- Member — Can view the connection status but cannot modify it.
Importing Events
Once connected, you can import individual Eventbrite events into the CRM:
Dashboard → Events → Import from Eventbrite
→ Select an event from the picker (live, started, or completed events)
→ Gordon CRM creates a CRM event record
→ Historical attendees are synced in the backgroundWhat Gets Imported
When you import an Eventbrite event, the following data is mapped to the CRM:
| Eventbrite Field | CRM Field | Notes |
|---|---|---|
name.text | name | Event title |
description.text | description | Plain text description |
venue.name or address.localized_address_display | location | Set to "Online" for virtual events |
start.utc | start_time | Stored in UTC |
end.utc | end_time | Stored in UTC |
venue.timezone | timezone | IANA timezone string, defaults to America/Chicago |
url | eventbrite_url | Direct link to the Eventbrite listing |
Lowest paid ticket_classes[] (cost + tax + fee) | ticket_price | (cost.actual.value + tax.value + fee.value) / 100. Excludes free and donation ticket classes. Fee is 0 when the organizer includes fees in the ticket cost. |
Count of paid ticket_classes[] | ticket_class_count | Number of non-free, non-donation ticket types. Used for "From $X" display logic. |
id | external_event_id | Links the CRM event to Eventbrite for webhook matching |
Duplicate Prevention
Each Eventbrite event can only be imported once per workspace. If you attempt to import an event that already exists, the system returns an error with the existing event's name.
Historical Attendee Sync
On import, the system performs a historical sync that paginates through all existing attendees for the Eventbrite event. For each attendee:
-
Contact Upsert — The attendee's email, first name, and last name are upserted into the
contactstable withsource = eventbrite. Existing contacts are updated (not duplicated). -
Registration Record — A registration row is created with the attendee's payment amount and status. The system protects against data reversal: if a contact has both an active ticket and a cancelled ticket (from pagination edge cases), the active
registeredstatus always takes priority.
Syncing Events
After an Eventbrite event has been imported, it can be re-synced to pull the latest data from Eventbrite. The sync updates all Eventbrite-owned fields:
name,description,locationstart_time,end_time,timezoneeventbrite_urlticket_price,ticket_class_count
User-managed fields are never overwritten by a sync:
website_title,website_description,is_published
After updating the event record, the sync re-processes all attendees using the same contact
upsert and registration deduplication logic as the initial import. The last_synced_at timestamp
is updated after each sync.
Webhook Processing
After an event is imported, all future registration activity is synced in real-time through Eventbrite webhooks. The webhook handler lives at:
POST /api/webhooks/eventbriteArchitecture: Immediate 200 Response
The webhook handler returns 200 immediately before processing the payload. All data synchronization
runs in the background using Next.js's after() API. This prevents Eventbrite from timing out and
retrying (which would cause duplicate processing).
Supported Webhook Actions
| Action | Description |
|---|---|
order.placed | A new ticket order has been completed |
order.refunded | A ticket order has been refunded (full or partial) |
attendee.updated | An attendee's details have changed (e.g. ticket transfer) |
Connection Lookup
When a webhook arrives, the handler identifies which workspace it belongs to:
- Primary lookup — Matches the
webhook_idfrom the payload config againsteventbrite_connections. - Fallback — If no match is found (e.g. webhook was re-created), the handler iterates through all
connections and tests each token against the
api_urlto find the one that authenticates successfully.
Order Processing (order.placed / order.refunded)
For order-based webhooks, the handler:
- Fetches the full order from Eventbrite with
?expand=attendees. - Looks up the CRM event by matching
external_event_idto the Eventbrite event ID. If the event isn't tracked in the CRM, the webhook is silently skipped. - Processes each attendee in the order individually.
Per-Attendee Processing
For each attendee in the order:
| Step | Detail |
|---|---|
| Email Validation | Validates the attendee email. If invalid (e.g. placeholder "info requested"), falls back to the buyer's email and name. |
| Contact Upsert | Upserts the contact with source = eventbrite. |
| Amount Calculation | Extracts costs.gross.value and converts from cents to dollars. |
| Status Resolution | Trusts the individual attendee status, not the blanket order action. This is critical for partial refunds — order.refunded may fire but only some tickets are actually cancelled. |
| Registration Upsert | Upserts by external_attendee_id (idempotent, supports multi-ticket). |
Multi-Ticket Support
A single Eventbrite order can contain multiple tickets (attendees). Each ticket gets its own
event_registrations row, identified by a unique external_attendee_id. The unique index on
(workspace_id, external_attendee_id) ensures idempotent processing — processing the same webhook
twice produces the same result.
Technical note: The unique index does not use a
WHERE external_attendee_id IS NOT NULLclause. PostgreSQL treats NULLs as distinct in unique indexes, so historical rows (from manual events) with NULLexternal_attendee_idvalues don't conflict. A partial index was avoided because PostgREST's upsert cannot specify a WHERE clause in the ON CONFLICT target.
Automation Triggers
After processing a registration, the handler fires the event_registration
automation trigger — but only once per contact per webhook
invocation. If a single order contains multiple tickets for the same contact, the automation fires only
once (deduplicated via an in-memory Set<string>).
Refund & Cancellation Logic
When a ticket is refunded or cancelled:
- The registration's
statusis set tocancelledandamount_paidis zeroed out. - The handler checks whether the contact has any remaining registered tickets for the event.
- If the count is zero, all active or processing campaign enrollments linked to that event (via
trigger_event_id) are automatically cancelled.
This ensures that partially-refunded orders don't cancel campaigns prematurely — only a full refund (zero remaining tickets) triggers enrollment cancellation.
Attendee Updated (Ticket Transfers)
The attendee.updated webhook fires when an attendee's details change post-purchase, typically due to:
- Ticket transfers — The original buyer transfers a ticket to a new person.
- Info fill — The attendee fills in placeholder details after checkout.
The handler:
- Fetches the attendee from the Eventbrite API.
- Looks up the existing registration by
external_attendee_id. - If the attendee email has changed (ticket transfer), creates a new contact and reassigns the registration, then fires the automation trigger for the new contact.
- If only name/details changed, updates the existing contact record.
Auto-Subscribe
The Eventbrite connection has an optional auto-subscribe toggle that, when enabled, automatically subscribes contacts to marketing emails when they are created or updated via real-time webhooks.
The toggle is managed at Settings → Integrations and is only visible when Eventbrite is connected. Toggling it on displays a compliance confirmation modal:
"By enabling this, you are asserting that your Eventbrite attendees have given consent to receive marketing emails. This feature does not satisfy GDPR requirements for explicit opt-in consent. You are solely responsible for ensuring compliance with the laws applicable to your audience."
Toggling off does not require confirmation.
Webhook-Only Scope
Auto-subscribe applies exclusively to real-time webhook paths:
| Path | Auto-Subscribe? | Why |
|---|---|---|
order.placed webhook → syncOrderAttendees() | ✅ Yes | Real-time new registrations |
attendee.updated webhook (new contact or transfer) | ✅ Yes | Ticket transfers to a new person |
Historical import (importEventbriteEvent()) | ❌ No | Could pull hundreds of stale attendees |
Manual re-sync (syncEventbriteEvent()) | ❌ No | Re-processing existing data |
Why exclude historical imports? Historical imports can pull in attendees from events years ago. Auto-subscribing a cold list would trigger spam complaints and risk getting the Resend sending account suspended — affecting the entire platform, not just the offending workspace.
Complaint Guard
When auto-subscribe runs, it checks for complaint suppressions before subscribing:
- Query
contact_suppressionsfor a record withreason = 'complaint'on this contact. - If a complaint exists → skip — the contact is never re-subscribed.
- If no complaint → subscribe the contact and clear non-complaint suppressions (
bounce,unsubscribe,manual).
Consent Proof
Auto-subscribed contacts are recorded with:
| Field | Value |
|---|---|
is_subscribed | true |
opt_in_timestamp | Current timestamp |
opt_in_source | eventbrite_auto_subscribe |
opt_in_ip | null — the webhook IP is Eventbrite's server, not the attendee |
unsubscribed_at | Cleared |
RBAC
Toggling auto-subscribe requires Owner or Admin role.
Eventbrite Action Summary
| Eventbrite Event | CRM Effect |
|---|---|
| New order placed | Contact upserted, registration created, automations fire. If auto-subscribe is enabled, contact is subscribed to marketing (complaint guard applies). |
| Ticket refunded | Registration cancelled, campaigns cancelled if zero remaining tickets |
| Ticket transferred | New contact created, registration reassigned, automations fire for new contact. If auto-subscribe is enabled, new contact is subscribed (complaint guard applies). |
| Attendee info updated | Contact name/details updated (no automation re-fire unless it's a transfer) |
Related Documentation
- Events Overview — The event model, registration schema, and timezone handling.
- Automations → Triggers — The
event_registrationtrigger specification. - Automations → Actions — Campaign enrollment and tag assignment actions.