Guides
Events
Show events on your site

Show events on your site

Requires: A publishable key with READ_PUBLIC for reads; a secret key with WRITE_SALES for registration, checkout and donations. Member session optional.

This guide builds the event pages of your site from the community's events: a filterable list, an event page, free registration, invitations, the attendee's own status, and donations. Paid tickets go through checkout, covered in the Marketplace guide.

It assumes the server setup from Embed in your own code and its cobuntuForRequest() helper (the client, with the member's session when there is one).

GET  /api/v1/communities/:communityTag/events
GET  /api/v1/communities/:communityTag/events/facets
GET  /api/v1/communities/:communityTag/events/:idOrSlug
GET  /api/v1/communities/:communityTag/events/:idOrSlug/tiers
GET  /api/v1/communities/:communityTag/tiers/:tierId/form
GET  /api/v1/communities/:communityTag/tiers/:tierId/member-pricing/preview
POST /api/v1/communities/:communityTag/events/:idOrSlug/rsvp
GET  /api/v1/communities/:communityTag/events/:idOrSlug/invitations/check
POST /api/v1/communities/:communityTag/events/:idOrSlug/invitations/accept
GET  /api/v1/communities/:communityTag/events/:idOrSlug/attendance-status
POST /api/v1/communities/:communityTag/events/:idOrSlug/leave
POST /api/v1/communities/:communityTag/events/:idOrSlug/attendees/me/cancel-pending
POST /api/v1/communities/:communityTag/events/:slug/donate
GET  /api/v1/communities/:communityTag/events/:slug/donations/summary
GET  /api/v1/communities/:communityTag/magic-links/verify

Reads need READ_PUBLIC (the publishable key has it). Registration writes need WRITE_SALES. All amounts are integers in the smallest currency unit.

At a glance

curl "https://api.cobuntu.com/api/v1/communities/your-community/events?upcoming=true&limit=12" \
  -H "X-API-Key: $COBUNTU_PUBLISHABLE_KEY"

1. The list

const page = await cobuntu.events.list({ upcoming: true, limit: 12 });
// { events: Event[], hasMore: true, nextCursor: "eyJ..." }
 
const next = await cobuntu.events.list({ upcoming: true, limit: 12, cursor: page.nextCursor });

Every filter is applied before paging, so each page is full:

ParamMeaning
upcoming / pastNot ended / ended. Both together is 400 CONFLICTING_DATE_FILTERS.
qEvery word must match the name, description, location, tags or host.
category, subcategoryA category id from categories({ type: "EVENT" }).
from, toEvents overlapping the range. from = to = "2026-11-03" is that day.
pricefree or paid.
formatin_person, online or hybrid. A hybrid event counts for both others.
featuredOnly the featured event.
sortsoonest (default) or name. Keep it the same across pages.

Bad values answer 400 INVALID_INPUT with field naming the param.

Filter counts

events.facets() takes the scope params (upcoming, past, q, from, to, featured) and counts what each filter option would show:

const facets = await cobuntu.events.facets({ upcoming: true });
// { total: 4,
//   categories: [{ id: "cat_1", name: "Workshops", count: 2, subcategories: [] }],
//   uncategorized: 1,
//   price: { free: 2, paid: 2 },
//   format: { in_person: 2, online: 2, hybrid: 1, tba: 1 } }

Use the counts to hide options that would return nothing.

2. The event page

const client = cobuntuForRequest();          // with the member's session, if any
const event = await client.events.get(slug); // EventDetail
const tiers = await client.events.tiers(slug);

With a member session, tiers[i].memberPrice is the member's own price and isAttending tells you whether they are already registered. A members-only event answers 403 MEMBERS_ONLY without a session: send the visitor to sign in. onlineUrl is only filled for attendees.

Registration forms

A tier with hasForm: true collects answers. Read the fields and render them:

const form = await cobuntu.events.form(tier.id); // TierForm | null
// { id, title, fields: [{ id: "f_diet", type: "SELECT", label: "Diet", required: true, options: [...] }] }

Send the answers keyed by field id as formAnswers. Missing required answers are 400 FORM_ANSWERS_REQUIRED with details.missingFields.

What members would pay

events.memberPricingPreview(tierId) answers "join and save" questions for a visitor: the list price and the best member offer, with how to join. See Join to Save.

3. Free RSVP

const r = await cobuntu.events.rsvp(slug, {
  email: "alex@example.com",
  name: "Alex Doe",
  tierId: tier.id,
  formAnswers: { f_diet: "vegetarian" },
});
// { attendanceId: "att_1", status: "APPROVED", alreadyRegistered: false }

status is PENDING when the event requires approval. With a donation the RSVP becomes a Stripe checkout instead, and successUrl / cancelUrl are required:

const r = await cobuntu.events.rsvp(slug, {
  email, tierId, donation: { amount: 500 },
  successUrl: returnUrl(origin, "/events/thanks"),
  cancelUrl: returnUrl(origin, `/events/${slug}`),
});
if ("checkoutUrl" in r) redirect(r.checkoutUrl);

4. Invitations

An invitation email carries a token. Check it before showing the accept button:

const inv = await cobuntu.events.invitations.check(slug, token);
// { valid: true, status: "PENDING", email: "alex@example.com", eventSlug: "summer-meetup", requiresPayment: false }

Accept it. A guest sends the invited email; a member uses client.as():

const r = await cobuntu.events.invitations.accept(slug, { token, email: inv.email });
if (r.status === "needs_payment") {
  // A paid tier: checkout with the invitation attached.
  await cobuntu.events.checkout(slug, { tierId, invitationToken: r.invitationToken, successUrl, cancelUrl });
}

5. "Am I registered?"

events.attendanceStatus has three modes, chosen by what you pass:

CallerPassAnswer
Signed-in membernothing (client.as(session))Full status, lookupMode: "member"
Guest with their emailed manage link{ token }Full status, lookupMode: "guest_token"
Anyone with an email{ email }Only registered: true | false
const s = await cobuntu.as(session).events.attendanceStatus(slug);
// { attendanceId: "att_1", status: "APPROVED", cancellationReason: null, lookupMode: "member" }
 
const g = await cobuntu.events.attendanceStatus(slug, { email: "alex@example.com" });
// { attendanceId: null, status: null, cancellationReason: null, registered: true, lookupMode: "guest" }

The email lookup never reveals the status itself, and it is limited per IP. Use it to say "you are already registered, check your inbox", not to show a ticket.

The guest manage link

Guests get a link with a token in their confirmation email. Resolve it to know what the link is for:

const ctx = await cobuntu.magicLinks.verify(token);
// { purpose: "event-cancel", eventSlug, eventName, startDate, email, saleId, attendanceId, refundEligible, refundReason }

6. Leaving

// Member
await cobuntu.as(session).events.leave(slug);
// Guest, with the manage-link token
const r = await cobuntu.events.leave(slug, { token });
// { message, wasPaidAttendee: true, refundProcessed: true, refundAmount: 1500 }

A paid ticket outside the refund window refuses to leave unless you pass forceWithoutRefund: true; show the refund rules and ask first.

A member whose registration is still waiting for approval cancels it with events.cancelPending(slug) (session required). If they paid, the hold is released.

7. Donations

A standalone donation to an event, without a ticket:

const { checkoutUrl } = await cobuntu.events.donate(slug, {
  donation: { amount: 1000 },
  customer: { email: "alex@example.com" }, // required without a session
  successUrl: returnUrl(origin, "/thanks"),
  cancelUrl: returnUrl(origin, `/events/${slug}`),
});
redirect(checkoutUrl);

A public total for a progress bar:

const sum = await cobuntu.events.donationsSummary(slug);
// { total: 45000, count: 31, currency: "EUR" }

Codes: 400 INVALID_DONATION_AMOUNT, 404 for an unknown event. Donations are limited per IP.