Events
Eventbrite Integration

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 table

The callback handler performs five steps:

  1. Token Exchange — Exchanges the authorization code for a long-lived access token.
  2. Organization Discovery — Fetches the user's Eventbrite organization ID and name.
  3. Webhook Registration — Creates a webhook subscription on the Eventbrite organization to receive order.placed, order.refunded, and attendee.updated events.
  4. Connection Storage — Persists the access token, organization info, and webhook ID in the eventbrite_connections table.
  5. 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:

ColumnTypeDescription
iduuidPrimary key
workspace_iduuidFK → workspaces, unique
access_tokentextEventbrite OAuth access token
organization_idtextEventbrite organization ID
organization_nametextHuman-readable org name for display
webhook_idtextEventbrite webhook subscription ID
auto_subscribe_attendeesbooleanWhen true, webhook-created contacts are auto-subscribed to marketing. Default false.
connected_attimestamptzWhen the connection was established

Disconnecting

Disconnecting Eventbrite from a workspace:

  1. Deletes the webhook subscription on Eventbrite's side (best-effort — non-critical if it fails).
  2. Removes the eventbrite_connections row.

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 background

What Gets Imported

When you import an Eventbrite event, the following data is mapped to the CRM:

Eventbrite FieldCRM FieldNotes
name.textnameEvent title
description.textdescriptionPlain text description
venue.name or address.localized_address_displaylocationSet to "Online" for virtual events
start.utcstart_timeStored in UTC
end.utcend_timeStored in UTC
venue.timezonetimezoneIANA timezone string, defaults to America/Chicago
urleventbrite_urlDirect 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_countNumber of non-free, non-donation ticket types. Used for "From $X" display logic.
idexternal_event_idLinks 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:

  1. Contact Upsert — The attendee's email, first name, and last name are upserted into the contacts table with source = eventbrite. Existing contacts are updated (not duplicated).

  2. 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 registered status 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, location
  • start_time, end_time, timezone
  • eventbrite_url
  • ticket_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/eventbrite

Architecture: 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

ActionDescription
order.placedA new ticket order has been completed
order.refundedA ticket order has been refunded (full or partial)
attendee.updatedAn attendee's details have changed (e.g. ticket transfer)

Connection Lookup

When a webhook arrives, the handler identifies which workspace it belongs to:

  1. Primary lookup — Matches the webhook_id from the payload config against eventbrite_connections.
  2. 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_url to find the one that authenticates successfully.

Order Processing (order.placed / order.refunded)

For order-based webhooks, the handler:

  1. Fetches the full order from Eventbrite with ?expand=attendees.
  2. Looks up the CRM event by matching external_event_id to the Eventbrite event ID. If the event isn't tracked in the CRM, the webhook is silently skipped.
  3. Processes each attendee in the order individually.

Per-Attendee Processing

For each attendee in the order:

StepDetail
Email ValidationValidates the attendee email. If invalid (e.g. placeholder "info requested"), falls back to the buyer's email and name.
Contact UpsertUpserts the contact with source = eventbrite.
Amount CalculationExtracts costs.gross.value and converts from cents to dollars.
Status ResolutionTrusts 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 UpsertUpserts 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 NULL clause. PostgreSQL treats NULLs as distinct in unique indexes, so historical rows (from manual events) with NULL external_attendee_id values 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:

  1. The registration's status is set to cancelled and amount_paid is zeroed out.
  2. The handler checks whether the contact has any remaining registered tickets for the event.
  3. 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:

  1. Fetches the attendee from the Eventbrite API.
  2. Looks up the existing registration by external_attendee_id.
  3. 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.
  4. 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:

PathAuto-Subscribe?Why
order.placed webhook → syncOrderAttendees()✅ YesReal-time new registrations
attendee.updated webhook (new contact or transfer)✅ YesTicket transfers to a new person
Historical import (importEventbriteEvent())❌ NoCould pull hundreds of stale attendees
Manual re-sync (syncEventbriteEvent())❌ NoRe-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:

  1. Query contact_suppressions for a record with reason = 'complaint' on this contact.
  2. If a complaint exists → skip — the contact is never re-subscribed.
  3. If no complaint → subscribe the contact and clear non-complaint suppressions (bounce, unsubscribe, manual).

Consent Proof

Auto-subscribed contacts are recorded with:

FieldValue
is_subscribedtrue
opt_in_timestampCurrent timestamp
opt_in_sourceeventbrite_auto_subscribe
opt_in_ipnull — the webhook IP is Eventbrite's server, not the attendee
unsubscribed_atCleared

RBAC

Toggling auto-subscribe requires Owner or Admin role.


Eventbrite Action Summary

Eventbrite EventCRM Effect
New order placedContact upserted, registration created, automations fire. If auto-subscribe is enabled, contact is subscribed to marketing (complaint guard applies).
Ticket refundedRegistration cancelled, campaigns cancelled if zero remaining tickets
Ticket transferredNew contact created, registration reassigned, automations fire for new contact. If auto-subscribe is enabled, new contact is subscribed (complaint guard applies).
Attendee info updatedContact name/details updated (no automation re-fire unless it's a transfer)

Related Documentation