Show events on your site
Requires: A publishable key with
READ_PUBLICfor reads; a secret key withWRITE_SALESfor 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/verifyReads 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:
| Param | Meaning |
|---|---|
upcoming / past | Not ended / ended. Both together is 400 CONFLICTING_DATE_FILTERS. |
q | Every word must match the name, description, location, tags or host. |
category, subcategory | A category id from categories({ type: "EVENT" }). |
from, to | Events overlapping the range. from = to = "2026-11-03" is that day. |
price | free or paid. |
format | in_person, online or hybrid. A hybrid event counts for both others. |
featured | Only the featured event. |
sort | soonest (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:
| Caller | Pass | Answer |
|---|---|---|
| Signed-in member | nothing (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.