How to build a community event calendar with Webflow and Eventbrite

Learn how to pull Eventbrite events into a Webflow Cloud Route Handler, keep the private token server-side, and stay inside the hourly API rate limit.

How to build a community event calendar with Webflow and Eventbrite

Ismail Ajagbe
Technical Author
View author profile
Ismail Ajagbe
Technical Author
View author profile
Table of contents

By integrating Eventbrite's robust event management directly into your Webflow site, you can create a seamless, real-time community calendar that empowers your brand while automating updates.

A community calendar has a specific failure mode. Events live in Eventbrite because that is where ticketing, refunds, and attendee data belong. The site lives in Webflow because that is where the brand lives, and sometimes, the two can drift apart the moment somebody reschedules something.

This guide pulls events from Eventbrite through your own Route Handler on Webflow Cloud, so the calendar reflects what Eventbrite currently says without anyone maintaining a second copy.

What do you need to connect Eventbrite and Webflow?

You need an Eventbrite organization with events, a private token, and a Webflow Cloud project to call the API from. The token dictates the architecture, because Eventbrite is explicit about where it cannot go.

Here’s the full list before you start:

  • An Eventbrite account with an organization that owns the events you want to list
  • A private token from your Eventbrite API Keys page, plus the organization ID
  • A Webflow Cloud project running Next.js 15 or higher, with Node.js 22 or later locally
  • A page or Embed element in the Designer where the rendered calendar will sit

You should never put a private token in client-side code, and you should delete tokens you no longer need. That single instruction is why this build has a server route. Here's how it fits together.

Which Eventbrite endpoints does a community calendar need?

Four endpoints cover what a community events page actually does, and picking the right one for each job keeps the integration within its request budget instead of fighting it.

The API base is https://www.eventbriteapi.com/v3, and these are the calls worth knowing:

What you want on the page Endpoint Why this one
Every event your organization runs /organizations/{id}/events/ The listing source for a community calendar, and paginated
One event's full detail /events/{id}/ Use expand= here rather than making follow-up calls
Ticket types and prices /events/{id}/ticket_classes/ Needed only if you render prices yourself rather than embedding checkout
Who registered /events/{id}/attendees/ Attendee data is private, so this stays server-side
What you want on the page → Endpoint → Why this one
Every event your organization runs
/organizations/{id}/events/
The listing source for a community calendar, and paginated
One event's full detail
/events/{id}/
Use expand= here rather than making follow-up calls
Ticket types and prices
/events/{id}/ticket_classes/
Needed only if you render prices yourself rather than embedding checkout
Who registered
/events/{id}/attendees/
Attendee data is private, so this stays server-side

The distinction that matters most is the last row. Attendee and order data is personal information about people who bought tickets, so it belongs in a server route with an authenticated audience, never in a public JSON response that happens to power a listing.

5 steps to build an Eventbrite calendar on Webflow Cloud

The build is a token in an environment variable, a fetch helper that asks for everything it needs in one call, a Route Handler that publishes a trimmed version, and a cache in front of it.

Each step protects either the token or the hourly request budget.

1. Create a private token and note the organization ID

Log in to Eventbrite, open your API Keys page, and copy the private token. While you are there, find the organization ID for the account that owns your events, since the organization is the level a community calendar lists from rather than a personal user.

Treat this token like a password. Eventbrite's own guidance is to keep it out of client-side code entirely and to delete any tokens you stop using, which is worth doing on a schedule rather than never.

If you have inherited a project with a token committed to a repository, rotate it before you do anything else, because a token in git history is a token that has been published.

Finish this step with two values written down, and neither in your codebase.

2. Store both values as Webflow Cloud environment variables

Open your Webflow Cloud environment and add the token and organization ID, marking the token as a Secret so it is redacted from build logs.

Webflow Cloud makes environment variables available during the build and to the deployed app at runtime so that the route can read them per request. Read them inside the handler rather than at module top level.

Do not prefix either variable with NEXT_PUBLIC_. That prefix is what tells Next.js a value is safe to ship to the browser, and applying it to an Eventbrite private token publishes the token to every visitor who opens developer tools. Changes to environment variables require a fresh deployment to take effect.

Finish this step with a successful deployment, and both variables listed in the Environment Variables tab, with the token marked Secret. Do not print them to check: Webflow treats redaction as a safety net rather than a handling workflow.

3. Fetch events with expansions rather than follow-up calls

The naive version of this integration lists events, then fetches each event's venue separately. That is one request per event, and it is how a calendar with forty events turns into forty-one requests every time somebody loads the page.

Eventbrite's expansions system solves it by letting you ask for related objects in the original call:

// lib/eventbrite.ts
const BASE = 'https://www.eventbriteapi.com/v3'

type EventbriteEvent = {
  id: string
  name: { text: string }
  start: { utc: string; local: string; timezone: string }
  url: string
  online_event: boolean
  venue?: { name: string; address: { localized_address_display: string } }
  logo?: { url: string }
}

export async function listOrganizationEvents(page = 1) {
  const orgId = process.env.EVENTBRITE_ORG_ID as string
  const token = process.env.EVENTBRITE_PRIVATE_TOKEN as string

  const url = new URL(`${BASE}/organizations/${orgId}/events/`)
  // 'started' matters: an event in progress leaves 'live'.
  url.searchParams.set('status', 'live,started')
  url.searchParams.set('order_by', 'start_asc')
  url.searchParams.set('page', String(page))
  // Pull the venue and logo in the same call rather than one
  // follow-up request per event.
  url.searchParams.set('expand', 'venue,logo')

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${token}` },
  })

  if (res.status === 429) {
    const detail = (await res.json().catch(() => null)) as
      | { error?: string; error_description?: string }
      | null
    // Branch on error, not error_description: the constant is stable.
    console.error('Eventbrite', detail?.error, detail?.error_description)
    throw new Error('EVENTBRITE_RATE_LIMITED')
  }

  if (!res.ok) {
    const detail = (await res.json().catch(() => null)) as
      | { error_description?: string }
      | null
    throw new Error(detail?.error_description ?? `Eventbrite ${res.status}`)
  }

  return (await res.json()) as {
    events: EventbriteEvent[]
    pagination: { has_more_items: boolean; page_number: number }
  }
}

The expand=venue,logo parameter is doing the real work here. Eventbrite documents venue and logo as Event expansions, though its own example uses the owned-events listing rather than the organization one, so confirm the objects come back on your endpoint before relying on them.

It is the difference between one request and one per event, and on a default budget of 2,000 calls per hour, that difference decides whether the page survives being shared.

Note the 429 branch too. Eventbrite returns HIT_RATE_LIMIT with a 429 when the hourly limit for a token is reached, and its error responses carry an error_description worth surfacing in logs rather than discarding. A successful call returns your events with venue and logo already attached.

4. Publish a trimmed response from a Route Handler

The route is the boundary between what Eventbrite knows and what the public page needs. Everything that crosses it should be deliberate.

Return only the fields the calendar renders:

// app/api/events/route.ts
import { NextResponse } from 'next/server'
import { getCloudflareContext } from '@opennextjs/cloudflare'
import { listOrganizationEvents } from '@/lib/eventbrite'

const CACHE_KEY = 'eventbrite:org-events'

export async function GET() {
  // Webflow Cloud replaces any Cache-Control response header with
  // private, no-cache, so cache in the KV store instead.
  const { env } = getCloudflareContext()
  const cache = env.EVENTS_CACHE

  const cached = await cache.get(CACHE_KEY)
  if (cached) {
    return NextResponse.json(JSON.parse(cached))
  }

  try {
    const { events } = await listOrganizationEvents()

    // Return only what the page renders. Attendee and order data
    // never belongs in a public response.
    const payload = {
      events: events.map((event) => ({
        id: event.id,
        title: event.name.text,
        startsAt: event.start.utc,
        startsAtLocal: event.start.local,
        timezone: event.start.timezone,
        url: event.url,
        venue:
          event.venue?.name ??
          (event.online_event ? 'Online' : 'Venue to be announced'),
        image: event.logo?.url ?? null,
      })),
    }

    // 60 seconds is the documented minimum TTL for the KV store.
    await cache.put(CACHE_KEY, JSON.stringify(payload), {
      expirationTtl: 60,
    })

    return NextResponse.json(payload)
  } catch (error) {
    if ((error as Error).message === 'EVENTBRITE_RATE_LIMITED') {
      return NextResponse.json({ error: 'Try again shortly' }, { status: 503 })
    }
    return NextResponse.json({ error: 'Could not load events' }, { status: 502 })
  }
}

One limit to be deliberate about: this returns only the first page. Eventbrite paginates in groups of 50, so a calendar with more events than that needs to follow the pagination through rather than stopping at the first response.

Mapping the response rather than forwarding it is a habit worth keeping. The raw event object carries far more than a listing needs, and a response that simply proxies the API tends to grow into an accidental public export of whatever Eventbrite adds next.

Caching is the other half of the rate limit story, and on Webflow Cloud it cannot be a response header. Webflow Cloud always replaces a response Cache-Control header with private, no-cache and strips it from requests, so a route that tries to cache that way is not caching at all.

Cache the payload in the Key Value Store instead, where 60 seconds is the documented minimum time to live. A minute of caching means a page shared to a few thousand people costs the same number of Eventbrite calls as a page nobody visits. You finish this step with a JSON endpoint your Designer page can fetch from.

5. Decide where checkout happens

You now have a calendar. The remaining decision is what a Register button does, and it is more consequential than it looks.

Sending visitors to the Eventbrite event URL means Eventbrite owns checkout, refunds, tax and attendee communication, which is usually the correct trade for a community event.

Embedding Eventbrite's checkout keeps people on your page while leaving the same responsibilities with Eventbrite. Building your own registration flow means you own all of it, including the parts nobody enjoys owning.

The middle option is the default for most community calendars, and the url field already in your trimmed response is what it needs. Whichever you choose, keep the ticket inventory in Eventbrite rather than mirroring counts into the CMS, since a stale "sold out" badge is worse than none.

You finish this step with a calendar whose buttons lead somewhere that can actually take money.

What causes Eventbrite integrations to fail on Webflow Cloud?

Failures here cluster around two things: the private token ending up somewhere it shouldn't, and the hourly request budget running out faster than anyone planned for. Neither announces itself clearly in the browser.

Four come up repeatedly, and the first is the one to check first.

Events stop loading during a traffic spike

Cause: The hourly rate limit. Eventbrite's default is 2,000 calls per hour per token, and it returns a 429 with HIT_RATE_LIMIT once you reach it. An uncached route that fetches per page view will hit it exactly when you most want the page to work.

Fix: Cache the Eventbrite payload in the Key Value Store rather than with a Cache-Control header, which Webflow Cloud replaces, and use expansions so one page render costs one call rather than one per event.

If you are still close to the ceiling after that, cache for longer rather than fetching more cleverly. A calendar that is sixty seconds out of date is fine; a calendar that returns errors when an event goes viral is not.

The token appears in the browser

Cause: The variable was prefixed with NEXT_PUBLIC_, or the fetch was moved into a client component during a refactor. Both ship the token to every visitor, and neither produces an error.

Fix: Keep the token in an unprefixed variable read inside the Route Handler, then check the built output rather than trusting the code. Search the deployed bundle for the token's first few characters, since that catches it whether it arrived through a prefix or through a client component.

If it was ever exposed, rotate it in Eventbrite rather than only removing it, because anything published has to be assumed collected. Eventbrite also advises deleting tokens you no longer need, which is the cheapest way to limit what a leaked one can still reach.

Venue or image data is missing from the listing

Cause: The expansion was not requested. Eventbrite doesn't return expanded objects by default, so event.venue is absent rather than empty, and code that optional-chains to it ends up with a blank field without erroring.

Fix: Add the object to the expand parameter, then confirm it in the raw response rather than in your rendered output, because an optional chain in your mapping code will hide the difference between absent and empty.

Handle the genuinely missing case too, since an online event has no venue at all, and a listing that prints an empty string looks broken rather than intentional. Falling back to a label such as Online is more honest than leaving the field blank and hoping nobody notices.

The build fails after adding the API routes

Cause: An export const runtime = 'edge' directive in one of the new files. Webflow Cloud deploys Next.js through the OpenNext Cloudflare adapter, which does not support the Next.js edge runtime.

Fix: Remove the line and redeploy, since Route Handlers already run on the Workers runtime. Search the whole project, not just the file you last touched, because one directive anywhere can fail the build.

Check this first whenever a Webflow Cloud build breaks immediately after new routes appear, since Webflow's own bring-your-own-app page still tells Next.js readers to add the directive, which means the instruction you followed may be the cause rather than your own code.

The error surfaces at bundling time and names the adapter, not the directive, so it rarely points at itself.

What you can build next with Eventbrite and Webflow

A live calendar is the foundation, not the finished thing. The obvious next moves are a past-events archive built from the same endpoint, filtering by category or venue, and a webhook that reacts when an event changes instead of waiting for the next cache expiry.

If you want registrations feeding an email audience as well as a ticketing platform, our Mailchimp event registration guide covers that path, including the consent question that comes with it. For the no-code connection routes, see the Webflow and Eventbrite integration.

Frequently asked questions

Can I call the Eventbrite API directly from the browser?

No. Eventbrite's documentation says explicitly not to put a private token in client-side code, and anything in a browser request is readable by the visitor. Call the API from a Route Handler and return only the fields your page renders.

How many Eventbrite API calls do I get?

The documented default is 2,000 calls per hour per token, and exceeding it returns a 429 with HIT_RATE_LIMIT. Caching the payload in Webflow Cloud's Key Value Store and using expansions keeps a busy calendar comfortably inside that. Note the second ceiling too: Eventbrite documents 48,000 calls per day alongside the hourly limit.

Should I copy events into the Webflow CMS instead?

Only if you need CMS features the API can't provide, such as Designer-styled rich content per event. A mirror needs a sync, and a silent sync failure leaves the site showing an event that moved. Reading live avoids that class of problem entirely.

What does the expand parameter actually save?

A request per related object. Without it, listing events and then fetching each venue is one call per event. With it, the venue arrives inside the original response, which is the difference between one call and forty-one on a busy calendar.

Where should ticket checkout happen?

Usually on Eventbrite, either by linking to the event URL or embedding their checkout. Both leave refunds, tax and attendee communication with the platform built for it. Build your own only when you need something Eventbrite's flow cannot express.


Last Updated
September 4, 2026
Category

Related articles


verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo

Get started for free

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.

Get started — it’s free
Watch demo

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.