REST API
Events

Events

Cobuntu events are dated gatherings — online, in-person, or hybrid. Each event can have multiple tiers (price points / ticket types). Members RSVP to free events directly via this API; paid tickets go through Checkout (separate flow, see Sales (orders + refunds) in the webhook catalog).

Public-read endpoints (READ_PUBLIC) — list events, get detail, list tiers, fetch tier forms. Write endpoint (WRITE_SALES) — free RSVPs.

Base URL: https://api.cobuntu.com/api/v1

Every endpoint requires an X-API-Key header. See Authentication for scope details.

GET /communities/{communityTag}/events — List events

Scope: READ_PUBLIC

Path parameters:

ParamTypeDescription
communityTagstringYour community's tag (e.g. your-community, acme).

Query parameters:

ParamTypeDefaultDescription
limitinteger20Page size. Default 20, max 100.
offsetinteger0Zero-based offset for pagination. Use either offset or cursor — not both.
featuredbooleanIf true, filter to the single featured event for this community.
upcomingbooleanIf true, filter to events with startDate >= now.
cursorstringOpaque cursor from a previous response's pagination.nextCursor. Use instead of offset for cursor-paginated endpoints.

Response (200):

[
  {
      "id": "…",
      "slug": "…",
      "name": "…",
      "description": "…"  // nullable,
      "startDate": "…",
      "endDate": "…"  // nullable,
      "physicalLocation": "…"  // nullable,
      "onlineUrl": "…"  // nullable,
      "bannerUrl": "…"  // nullable,
      "accessibility": "PUBLIC",
      "timezone": "Europe/Lisbon",
      "tiers": [<Tier>],
      "agenda": [<AgendaItem>],
      "featured": false,
    }
]
FieldTypeDescription
idstringStable event identifier (uuid).
slugstringURL-safe identifier, unique per community. Use this for deep-links.
namestringDisplay name shown on listings and detail pages.
descriptionstring (nullable)Long-form description (may contain markdown).
startDatestring (date-time)ISO 8601 start timestamp. UTC. Convert to your visitor's locale client-side.
endDatestring (date-time) (nullable)ISO 8601 end timestamp. Null for open-ended events.
timezonestring (IANA)The event's IANA timezone (e.g. Europe/Lisbon). Use it to render startDate/endDate/agenda times in local time.
physicalLocationstring (nullable)Physical venue — name and/or address (e.g. "Casa Tônico"). Null for online-only or TBD.
onlineUrlstring (nullable)Join / details URL for online events. Null for in-person events.
bannerUrlstring (nullable)Hero image URL (or null). Recommended display: 16:9.
accessibilitystring (enum)PUBLIC or MEMBERS_ONLY. MEMBERS_ONLY events return 404 on the public API for non-members.
tiersarrayActive price tiers for this event. Empty when the event isn't yet on sale.
agendaarrayOrdered agenda / itinerary items (see Agenda item). Empty [] when no agenda has been set.
featuredbooleanTrue if the event is the community's 'big frame' featured event. Only one event can be featured per community at a time.

Example:

curl https://api.cobuntu.com/api/v1/communities/my-community/events \
  -H "X-API-Key: pk_live_..."

GET /communities/{communityTag}/events/{slug} — Get event by slug

Scope: READ_PUBLIC

Path parameters:

ParamTypeDescription
communityTagstringYour community's tag (e.g. your-community, acme).
slugstringURL slug from the event / article. Stable across edits.

Response (200):

{
  "id": "…",
  "slug": "…",
  "name": "…",
  "description": "…"  // nullable,
  "startDate": "…",
  "endDate": "…"  // nullable,
  "physicalLocation": "…"  // nullable,
  "onlineUrl": "…"  // nullable,
  "bannerUrl": "…"  // nullable,
  "accessibility": "PUBLIC",
  "timezone": "Europe/Lisbon",
  "tiers": [<Tier>],
  "agenda": [<AgendaItem>],
  "featured": false,
}
FieldTypeDescription
idstringStable event identifier (uuid).
slugstringURL-safe identifier, unique per community. Use this for deep-links.
namestringDisplay name shown on listings and detail pages.
descriptionstring (nullable)Long-form description (may contain markdown).
startDatestring (date-time)ISO 8601 start timestamp. UTC. Convert to your visitor's locale client-side.
endDatestring (date-time) (nullable)ISO 8601 end timestamp. Null for open-ended events.
timezonestring (IANA)The event's IANA timezone (e.g. Europe/Lisbon). Use it to render startDate/endDate/agenda times in the event's local time.
physicalLocationstring (nullable)Physical venue — name and/or address (e.g. "Casa Tônico"). Null for online-only or TBD.
onlineUrlstring (nullable)Join / details URL for online events. Null for in-person events.
bannerUrlstring (nullable)Hero image URL (or null). Recommended display: 16:9.
accessibilitystring (enum)PUBLIC or MEMBERS_ONLY. MEMBERS_ONLY events return 404 on the public API for non-members.
tiersarrayActive price tiers for this event. Empty when the event isn't yet on sale.
agendaarrayOrdered agenda / itinerary items (see Agenda item). Empty [] when no agenda has been set.
featuredbooleanTrue if the event is the community's 'big frame' featured event. Only one event can be featured per community at a time.

Example:

curl https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID \
  -H "X-API-Key: pk_live_..."

Agenda item object

Each entry in an event's agenda array. Items are returned ordered (by order, then startTime). Times are instants serialized as ISO 8601 (UTC) — convert them to the event's timezone for display.

{
  "title": "Chegada e networking",
  "description": null,
  "startTime": "2026-06-08T17:00:00.000Z",
  "endTime": "2026-06-08T17:30:00.000Z",
  "order": 0
}
FieldTypeDescription
titlestringAgenda step label (e.g. "Palestra", "Jantar").
descriptionstring (nullable)Optional longer detail for the step.
startTimestring (date-time)ISO 8601 (UTC) start instant. Render in the event's timezone.
endTimestring (date-time)ISO 8601 (UTC) end instant.
orderintegerZero-based display order.

GET /communities/{communityTag}/events/{slug}/tiers — List tiers for an event

Scope: READ_PUBLIC

Path parameters:

ParamTypeDescription
communityTagstringYour community's tag (e.g. your-community, acme).
slugstringURL slug from the event / article. Stable across edits.

Response (200):

[
  {
      "id": "…",
      "name": "…",
      "priceCents": 0,
      "currency": "EUR",
      "capacity": 0  // nullable,
      "sold": 0,
      "isActive": false,
      "hasForm": false,
    }
]
FieldTypeDescription
idstringStable tier identifier (uuid).
namestringDisplay name (e.g. Early bird, VIP).
priceCentsintegerPrice in the smallest currency unit (cents for EUR/USD/GBP, no decimals for JPY).
currencystringISO 4217 code.
capacityinteger (nullable)Max attendees who can buy this tier. Null = unlimited.
soldintegerAttendees already on this tier. Read-only.
isActivebooleanWhether the tier accepts new RSVPs.
hasFormbooleanTrue if this tier requires answering a form at checkout (use GET /tiers/{tierId}/form to fetch the schema).

Example:

curl https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/tiers \
  -H "X-API-Key: pk_live_..."

POST /communities/{communityTag}/events/{slug}/rsvp — RSVP to a free event

Scope: WRITE_SALES

Registers the email-identified attendee (or the member identified by a forwarded auth/SSO token) to a free event. The free tier is auto-resolved as the event's first tier by sort order. For events with paid tiers, use /checkout instead.

This endpoint also supports donations on free events. When the event has donationConfig.enabled and the request body carries a donation field, the RSVP routes through Stripe Checkout instead of registering instantly — the buyer pays the donation, the seat is reserved on completion, and the host receives the donation net of Stripe's fee. See the two response shapes below.

Path parameters:

ParamTypeDescription
communityTagstringYour community's tag (e.g. your-community, acme).
slugstringURL slug from the event. Stable across edits.

Request body:

{
  "email": "…",
  "formAnswers": { },
  "donation": { "amount": 500 },
  "successUrl": "https://your-site.com/eventos/slug?rsvp=success&session={CHECKOUT_SESSION_ID}",
  "cancelUrl": "https://your-site.com/eventos/slug?rsvp=cancel"
}
FieldTypeDescription
emailstring (email)Required. Attendee email. Used for confirmation + reminder emails. Ignored when an authenticated member token identifies the buyer.
formAnswersobject (optional)Answers to the tier's form, if it has one.
donationobject (optional){ amount } in the donation currency's smallest unit (e.g. cents). Requires the event to have donationConfig.enabled. When present, the response is a Stripe Checkout URL (see below).
successUrlstring (required iff donation)Absolute URL to redirect to after a successful donation. Supports Stripe's {CHECKOUT_SESSION_ID} placeholder.
cancelUrlstring (required iff donation)Absolute URL to redirect to on cancellation.

Authenticated members (member-pricing-aware): forward the attendee's identity on this request via X-Cobuntu-Auth-Token (the value of the cobuntu_auth cookie) or X-Cobuntu-Member-Token (an SSO token). The backend verifies the token and attributes the RSVP to the member.

Response — no donation in body (200):

{
  "attendanceId": "…",
  "status": "APPROVED"
}
FieldTypeDescription
attendanceIdstringStable id for the new attendance. Idempotent on (eventId, email) — re-submitting returns the existing id.
statusstringAPPROVED for instant-registration events, PENDING for approval-gated events.

Response — donation in body (200):

{
  "checkoutUrl": "https://checkout.stripe.com/c/pay/…",
  "sessionId": "cs_…"
}
FieldTypeDescription
checkoutUrlstringRedirect the buyer here to complete the donation. The seat is reserved on Stripe webhook completion (checkout.session.completed); the confirmation email fires from the same code path as a regular free RSVP.
sessionIdstringStripe Checkout session id.

Common errors: 400 (INVALID_DONATION_AMOUNT, DONATION_REQUIRES_REDIRECT_URLS, missing email, PAID_TIER_REQUIRES_CHECKOUT, donations not configured, donation amount below Stripe's minimum of 50 smallest units), 404 (event not found), 409 (event full / capacity exhausted / already registered).

Examples:

Free RSVP (no donation):

curl -X POST \
  https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/rsvp \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "email": "fan@example.com" }'

Free RSVP with a donation:

curl -X POST \
  https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/rsvp \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "email": "fan@example.com",
    "donation": { "amount": 500 },
    "successUrl": "https://your-site.com/eventos/EXAMPLE_ID?rsvp=success",
    "cancelUrl": "https://your-site.com/eventos/EXAMPLE_ID?rsvp=cancel"
  }'

POST /communities/{communityTag}/events/{slug}/checkout — Buy a paid ticket

Creates a Stripe Checkout session for a paid event tier and returns the URL to redirect the buyer to. (For free tiers, use RSVP instead.) Requires the WRITE_SALES scope.

Member pricing: to charge a logged-in community member their member price, forward their identity on this request — either X-Cobuntu-Auth-Token (the value of the cobuntu_auth cookie) or X-Cobuntu-Member-Token (an SSO token). The backend verifies it against an ACCEPTED membership in this community and applies any matching member-pricing override; without it the buyer is charged the list price.

Request body:

{
  "tierId": "…",
  "quantity": 1,
  "customer": { "email": "…", "name": "…" },  // optional
  "successUrl": "https://your-site.com/eventos/slug?checkout=success&session={CHECKOUT_SESSION_ID}",
  "cancelUrl": "https://your-site.com/eventos/slug?checkout=cancel",
  "formAnswers": { },  // if the tier has a required form
  "donation": { "amount": 500 }  // optional, smallest currency unit
}
FieldTypeDescription
tierIdstringRequired. The paid tier to purchase. Must currently be on sale (else 409 TIER_NOT_ON_SALE).
quantityintegerTickets to buy. Defaults to 1; capped at 10 per session.
customerobject (optional){ email, name }. Ignored when the caller is an identified member; Stripe also collects email at the session.
successUrlstringRequired. Absolute URL to redirect to on success. Supports Stripe's {CHECKOUT_SESSION_ID} placeholder.
cancelUrlstringRequired. Absolute URL to redirect to on cancellation.
formAnswersobject (optional)Answers to the tier's form, if it has one.
donationobject (optional){ amount } (smallest unit) when the event accepts an optional donation alongside the ticket.

Response (200):

{
  "checkoutUrl": "https://checkout.stripe.com/c/pay/…",
  "sessionId": "cs_…"
}
FieldTypeDescription
checkoutUrlstringRedirect the buyer here to complete payment.
sessionIdstringStripe Checkout session id.

Common errors: 409 (TIER_NOT_ON_SALE, already has a ticket, or sold out), 400 (missing/invalid tierId / URLs), 404 (tier or event not found).

Example:

curl -X POST \
  https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/checkout \
  -H "X-API-Key: pk_live_..." \
  -H "Content-Type: application/json" \
  -H "X-Cobuntu-Auth-Token: <cobuntu_auth cookie value>" \
  -d '{ "tierId": "…", "quantity": 1, "successUrl": "https://…", "cancelUrl": "https://…" }'

POST /communities/{communityTag}/events/{slug}/donate — Donate without buying a ticket

Creates a Stripe Checkout session for a standalone donation. No ticket is reserved; the host receives the donation through the regular payout cycle. Requires the WRITE_SALES scope.

Use this when a buyer wants to support the event without paying for a seat — distinct from the donation sidecar on /checkout (which donates alongside a ticket purchase) and the donation sidecar on /rsvp (which donates alongside a free RSVP).

The endpoint validates the amount against event.donationConfig and rejects when donations aren't enabled. PWYW configs enforce Stripe's minimum charge (50 smallest currency units) since the donation is the entire session total.

Rate limit: 30 donations per IP per hour. Exceeded limits return 429 Too Many Requests.

Idempotency: the optional X-Idempotency-Key request header is forwarded to Stripe's session-create call. Re-submitting with the same key returns the same Checkout session instead of creating a duplicate.

Request body:

{
  "donation": { "amount": 500 },
  "customer": { "email": "donor@example.com" },
  "successUrl": "https://your-site.com/eventos/slug?donate=success&session={CHECKOUT_SESSION_ID}",
  "cancelUrl": "https://your-site.com/eventos/slug?donate=cancel"
}
FieldTypeDescription
donationobjectRequired. { amount } — positive integer in the donation currency's smallest unit. Validated against event.donationConfig.
customerobject (optional){ email }. Required for anonymous donors; ignored when an identified member is forwarded via the auth headers below.
successUrlstringRequired. Absolute URL to redirect to on success. Supports Stripe's {CHECKOUT_SESSION_ID} placeholder.
cancelUrlstringRequired. Absolute URL to redirect to on cancellation.

Headers (optional, member attribution): the same X-Cobuntu-Auth-Token / X-Cobuntu-Member-Token headers documented on /checkout attribute the donation to a known member. Without them the donor is anonymous (email-only).

Response (200):

{
  "checkoutUrl": "https://checkout.stripe.com/c/pay/…",
  "sessionId": "cs_…"
}

Common errors: 400 (INVALID_DONATION_AMOUNT, donations not configured, amount below Stripe's 50-smallest-unit minimum, amount not in the fixed-mode list, missing customer.email for anonymous donors), 403 (event is members-only — auth header required), 404 (event not found), 429 (rate-limited).

Example:

curl -X POST \
  https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/donate \
  -H "X-API-Key: pk_live_..." \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -d '{
    "donation": { "amount": 500 },
    "customer": { "email": "donor@example.com" },
    "successUrl": "https://your-site.com/eventos/EXAMPLE_ID?donate=success",
    "cancelUrl": "https://your-site.com/eventos/EXAMPLE_ID?donate=cancel"
  }'

GET /communities/{communityTag}/events/{slug}/donations/summary — Aggregated donation totals

Returns the running totals for all donations attached to this event — across all three flows (sidecar on a paid ticket, free-RSVP-with-donation, and standalone). Requires the READ_PUBLIC scope.

Anonymized at this scope: returns aggregates only, never individual donor identities. Use it to render "€420 raised across 18 donations" on a public-facing fundraising widget.

Response (200):

{
  "total": 42000,
  "count": 18,
  "currency": "EUR"
}
FieldTypeDescription
totalintegerSum of donation amounts in the currency's smallest unit. Returns 0 when no donations exist yet.
countintegerNumber of donation rows attached to the event.
currencystring | nullISO 4217 currency code (e.g. EUR). Null when no donations exist yet.

Example:

curl https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/donations/summary \
  -H "X-API-Key: pk_live_..."

GET /communities/{communityTag}/tiers/{tierId}/form — Tier form schema

Scope: READ_PUBLIC

Returns the form schema attached to a tier (or null if the tier has no form). Use this to render form fields on your custom checkout page before the buyer hits /checkout or /rsvp — the answers are then submitted as the formAnswers field on that subsequent call.

The form schema is opaque to Cobuntu once submitted. Cobuntu stores answers keyed by the field id strings the schema declares; how those answers are displayed in the host's admin (and emailed back to the buyer in the confirmation) is driven by the schema's own label strings.

The schema lives on the tier, not the event — the same tier can be cloned across many events and the form follows it.

Path parameters:

ParamTypeDescription
communityTagstringYour community's tag.
tierIdstringThe tier id from tiers[*].id on the event-detail response or from /events/{slug}/tiers.

Response (200) — tier has a form:

{
  "tierId": "…",
  "formData": {
    "fields": [
      { "id": "f-name",  "type": "SHORT_TEXT", "label": "Full name",   "required": true },
      { "id": "f-phone", "type": "PHONE",      "label": "Phone number", "required": false },
      { "id": "f-diet",  "type": "LONG_TEXT",  "label": "Dietary requirements" }
    ]
  }
}

Response (200) — tier has no form:

{ "tierId": "…", "formData": null }
FieldTypeDescription
tierIdstringEcho of the path parameter.
formDataobject | nullThe opaque schema blob. null means no form is configured for this tier — your checkout page can skip rendering fields entirely.
formData.fields[]arrayThe form fields, in display order. Render in the order returned.
formData.fields[].idstringStable field id. Use this as the key when submitting formAnswers to /rsvp or /checkout.
formData.fields[].typestringOne of SHORT_TEXT, LONG_TEXT, EMAIL, PHONE, NUMBER, SELECT, CHECKBOX. Drives input rendering on your end.
formData.fields[].labelstringHuman-readable label. Shown to buyers; echoed back in the host's attendee CSV export.
formData.fields[].requiredboolean (optional)When true, omit this field at submit-time and /rsvp or /checkout will respond 400.

Defence-in-depth: the endpoint verifies the tier belongs to an event in the API key's community. Tiers from another community return 404 even with a valid key — preventing cross-community schema enumeration.

Examples:

curl https://api.cobuntu.com/api/v1/communities/my-community/tiers/TIER_ID/form \
  -H "X-API-Key: pk_live_..."

Round-trip with /rsvp:

# 1. Fetch the schema
SCHEMA=$(curl -s https://api.cobuntu.com/api/v1/communities/my-community/tiers/TIER_ID/form \
  -H "X-API-Key: pk_live_...")
 
# 2. Render the fields in your UI, collect answers keyed by field id…
# 3. Submit on RSVP
curl -X POST https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/rsvp \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "email": "fan@example.com",
    "formAnswers": { "f-name": "Ana Costa", "f-phone": "+351 912 345 678" }
  }'

Buyer self-service endpoints

The endpoints in this section let your integrator app offer the same self-service affordances Cobuntu's own community-app exposes on the post-checkout receipt page — cancel, refund, check status — without forcing the buyer to bounce to cobuntu.com.

All four endpoints sit behind your existing API key (WRITE_SALES for the writes; READ_PUBLIC for the status read) and re-use the auth identity headers you already forward for member pricing:

Buyer typeHow they identifyWhere the token lives
Member (logged into the community on a customer apex)X-Cobuntu-Auth-Token headerRead from the cobuntu_auth_<tag> cookie set after /login
Member (post-SSO handshake)X-Cobuntu-Member-Token headerURL fragment #cobuntu_session=… captured by your app, persisted in storage
Guest (email-only, no Cobuntu account)token in request bodySigned MagicLink token emailed at registration time (see Magic-link tokens for guests below)

Payment-bypass invariant: none of these endpoints can create attendance or skip checkout. They either transition existing attendance state (cancel) or move money back through Stripe (refund). New attendance always flows through /rsvp or /checkout.

GET /communities/{communityTag}/events/{slug}/attendance-status — Look up the buyer's attendance

Scope: READ_PUBLIC

Use this to render a "you're already registered" badge on a return visit to your event page — and to discover the attendanceId your app needs for cancellation actions. Two read modes:

  • Member: forward X-Cobuntu-Auth-Token or X-Cobuntu-Member-Token. Returns the member's attendance state.
  • Guest: append ?email=<buyer-email> to the URL. Returns whether a guest attendance exists for that email on this event.

Returns 200 with all-null fields when no attendance row exists for the caller (it's not a 404 — the event itself is fine, the caller just hasn't registered yet).

Path parameters:

ParamTypeDescription
communityTagstringYour community's tag.
slugstringThe event slug.

Query parameters:

ParamTypeDescription
emailstring (email) (guest-only)Required when no X-Cobuntu-Member-Token / X-Cobuntu-Auth-Token is forwarded. Case-insensitive match against the guest attendance row. Pass it through directly — the response never echoes PII back.

Response (200):

{
  "attendanceId": "…",
  "status": "APPROVED",
  "cancellationReason": null,
  "lookupMode": "member"
}
FieldTypeDescription
attendanceIdstring | nullThe attendance row id. null when no row exists for this caller — render "Register" UI.
statusstring | nullOne of APPROVED, PENDING, REJECTED, CANCELLED. null when no row.
cancellationReasonstring | nullWhen status === 'CANCELLED', the cause: USER_REQUEST, ADMIN_REMOVAL, or PAYMENT_TIMEOUT. Use it to surface cause-specific copy in your UI.
lookupModestring"member" when resolved via tokens, "guest" when resolved via ?email=. Lets you branch UI logic.

Errors: 401 (no member token and no ?email), 404 (event slug not found).

Examples:

Member lookup:

curl https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/attendance-status \
  -H "X-API-Key: pk_live_..." \
  -H "X-Cobuntu-Member-Token: $MEMBER_TOKEN"

Guest lookup:

curl "https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/attendance-status?email=fan@example.com" \
  -H "X-API-Key: pk_live_..."

POST /communities/{communityTag}/events/{slug}/leave — Buyer self-leaves an event

Scope: WRITE_SALES

Cancels the buyer's attendance. Works for both free and paid attendances — paid uses Cobuntu's existing 7-day refund-eligibility window (when the buyer's still inside the window, the cancellation is paired with an automatic Stripe refund net of fees; outside the window, set forceWithoutRefund to cancel anyway).

The attendance row is preserved as status: 'CANCELLED' with cancellationReason: 'USER_REQUEST' for audit, not deleted. The next attendance-status call returns the cancelled row.

Use this for APPROVED attendances. For PENDING reservations (Reserve-Before-Decide buyers who haven't been approved yet), use /attendees/me/cancel-pending instead — the refund math differs.

Request body (member): empty {} is fine. Optionally { "forceWithoutRefund": true }.

Request body (guest):

{
  "token": "…",
  "forceWithoutRefund": false
}
FieldTypeDescription
tokenstring (guest-only)Signed MagicLink token emailed at registration. Required for guest leaves. See Magic-link tokens for guests.
forceWithoutRefundboolean (optional)When true, skips the refund attempt and just cancels the attendance. Use it after a REFUND_NOT_ELIGIBLE response to let the buyer walk away without their money back.

Response (200):

{
  "message": "Successfully left the event",
  "wasPaidAttendee": true,
  "refundProcessed": true,
  "refundAmount": 4500
}
FieldTypeDescription
messagestringHuman-readable confirmation. Surface as-is or replace with your own copy.
wasPaidAttendeebooleantrue if the attendance was a paid ticket.
refundProcessedbooleantrue when the Stripe refund went through.
refundAmountinteger | nullRefund amount in the currency's smallest unit (e.g. cents). Net of Stripe processing fees per T&Cs §7.6.

Errors:

StatusCodeMeaning
400REFUND_NOT_ELIGIBLEBuyer is outside the 7-day refund window or the sale is no longer in ESCROW. Response includes eligibility, saleId, saleAmount, and canForceLeave: true. Re-call with forceWithoutRefund: true to proceed without a refund.
401No member token AND no body token.
403Guest token doesn't match this event.
500REFUND_FAILEDStripe refused the refund mid-flow. The buyer's money is stuck; this is escalated server-side.

Examples:

Member leaves:

curl -X POST https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/leave \
  -H "X-API-Key: sk_live_..." \
  -H "X-Cobuntu-Member-Token: $MEMBER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

Guest leaves (with the MagicLink token they received in their receipt email):

curl -X POST https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/leave \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "token": "ey…" }'

POST /communities/{communityTag}/events/{slug}/attendees/me/cancel-pending — Self-cancel a PENDING reservation

Scope: WRITE_SALES

When the event is paid + requires host approval, Cobuntu's checkout holds the buyer's money in escrow as a PENDING reservation until the host decides. This endpoint lets the buyer withdraw their application before the host responds — they get refunded net of Stripe fees, and the seat is released back to the pool.

Member-only — guest support requires a token shape Cobuntu doesn't yet emit at reservation time. The buyer is identified by X-Cobuntu-Member-Token or X-Cobuntu-Auth-Token; the controller resolves their PENDING attendance for this event server-side, so you don't need to pass the attendanceId.

Request body: empty {}.

Response (200):

{
  "cancelled": true,
  "refundIssued": true,
  "refundAmountCents": 4500,
  "refundCurrency": "EUR",
  "stripeFeeCents": 250
}
FieldTypeDescription
cancelledbooleanAlways true on a 200 response.
refundIssuedbooleantrue when Stripe accepted the refund.
refundAmountCentsinteger | nullWhat the buyer received back, in smallest unit.
refundCurrencystring | nullISO 4217 code (e.g. EUR).
stripeFeeCentsinteger | nullFee Stripe kept (your buyer absorbed). Surface this in your confirmation toast: "You'll receive €45 back (€2.50 in processing fees)".

Errors:

StatusMeaning
401No member token forwarded.
404No PENDING reservation found for this buyer + event. (E.g. the host already approved them — use /leave instead.)
409The attendance exists but is in a status other than PENDING (e.g. CANCELLED already, or APPROVED post-host-decision).

Example:

curl -X POST https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/attendees/me/cancel-pending \
  -H "X-API-Key: sk_live_..." \
  -H "X-Cobuntu-Member-Token: $MEMBER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

POST /communities/{communityTag}/events/{slug}/sales/me/refund — Buyer self-refunds a paid ticket

Scope: WRITE_SALES

For a buyer who already attended (or no longer plans to attend) a paid event and qualifies for a refund under the 7-day window + ESCROW payout status, this endpoint creates a refund request that processes through Stripe. Mirrors what the in-app "Cancel registration & refund" button does for buyers on cobuntu.com.

Member-only. Ownership + community + event scope are all gated server-side: the sale must belong to the authenticated buyer AND be for this event AND be in this community. Cross-community / cross-event saleIds return 404.

When to use which endpoint:

Buyer stateEndpoint
PENDING reservation (host hasn't decided)/attendees/me/cancel-pending
APPROVED free attendance/leave
APPROVED paid attendance, within refund window/leave (handles both cancel + refund atomically) or this endpoint (refund only — the attendance row stays open)
APPROVED paid attendance, partial refundThis endpoint (with amount)

Request body:

{
  "saleId": "…",
  "reason": "…",
  "amount": 25.00,
  "refundType": "BUYER_REQUEST"
}
FieldTypeDescription
saleIdstringRequired. The sale to refund. Get it from attendance-status (cross-reference via your own checkout-completion bookkeeping) or by calling /sales/me on the in-app surface.
reasonstringRequired. Free-text reason. Stored on the refund row for the host's audit log.
amountnumber (optional)Refund amount in major units (e.g. 25.00 for €25). Omit for a full refund. Capped at 1_000_000. Server multiplies by 100 for storage.
refundTypestring (optional)Defaults to BUYER_REQUEST. The only other valid values come from admin paths and are rejected here.

Response (200):

The refund result from RefundService.createRefundRequest — fields vary slightly by refund flow but always include id, status, and amount. See errors.mdx for downstream not-eligible shapes.

Errors:

StatusMeaning
400Missing saleId / reason, invalid amount, or refund not eligible (outside 7-day window, sale already refunded, etc.).
401No member token forwarded.
403The authenticated buyer doesn't own this sale.
404saleId doesn't exist, belongs to another community, or isn't for the event in the URL slug.

Example:

curl -X POST https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/sales/me/refund \
  -H "X-API-Key: sk_live_..." \
  -H "X-Cobuntu-Member-Token: $MEMBER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "saleId": "sale_…",
    "reason": "Plans changed — cannot attend",
    "refundType": "BUYER_REQUEST"
  }'

Magic-link tokens for guests

The two write endpoints above (/leave, /attendees/me/cancel-pending) need to know which attendance row to act on. For members, the SSO/auth tokens carry the user identity. For guests (email-only, no Cobuntu account), Cobuntu emails them a signed token at registration time and your integration can deep-link that token back to your own cancellation page.

Two token types are accepted, both produced by existing Cobuntu email flows:

TypeWhat it carriesEmitted by
event-cancelattendanceId + guestEmail directlyFree-RSVP receipt + R-B-D reservation receipt
sale-managesaleId + guestEmail (attendance resolved server-side)Paid-ticket purchase receipt

In Cobuntu's own email templates, these tokens are embedded in URLs that point at cobuntu.com/events/{slug}/cancel?token=…. To route them through your app instead:

  1. Your email template (set in admin) includes a deep link to your own cancellation page: https://your-site.com/events/{slug}/cancel?token={token}.
  2. Your page extracts the token query parameter and POSTs it to /leave (or /attendees/me/cancel-pending if the buyer is PENDING).
  3. The buyer never leaves your app.

The tokens are tied to a specific event — passing a token for event A to event B's /leave returns 403. Tokens for sales must additionally match the guestEmail claim against the sale's actual guestEmail (defense-in-depth against stale or leaked tokens after the buyer rotates their email).

Tokens expire on the same schedule Cobuntu's MagicLinkService uses today. Invalid or expired tokens return 401.


Errors — /rsvp reference

/rsvp rejects requests that violate registration invariants. All errors follow the standard envelope ({ error, code? }):

StatusCode (or shape)CauseResolution
400PAID_TIER_REQUIRES_CHECKOUTTier resolved to a price > 0 — /rsvp is free-only.Switch to /checkout.
400INVALID_DONATION_AMOUNTdonation.amount is not a positive integer, or below Stripe's 50-smallest-unit minimum.Render an input validator that mirrors the constraint.
400DONATION_REQUIRES_REDIRECT_URLSdonation present without successUrl / cancelUrl, or URLs aren't absolute.Include both URLs starting with http(s)://.
400error: email is requiredBody missing email AND no member token forwarded.Either collect an email or forward the buyer's token.
400error: Event has no tiersThe event hasn't been configured with at least one tier.Surface "registration not open yet"; the host needs to publish a tier.
400error: Donations are not configured…donation body field present, but donationConfig.enabled === false on this event.Check donationConfig on the event GET first.
400error: missing form answersThe tier has a required form and formAnswers is missing or incomplete.Call /tiers/{tierId}/form first and render the required fields.
404error: Event not foundSlug doesn't exist OR event is MEMBERS_ONLY and caller is anonymous (404 instead of 403 — the public API doesn't acknowledge gated events to anonymous callers).Verify the slug; for member-gated events, forward an X-Cobuntu-Member-Token.
409error contains already registered / already hasIdempotency hit — this email or user already has an attendance for this event.Surface "you're already registered" + (optionally) call /attendance-status to confirm.
409error contains full / capacityAll seats are taken.Render "Event full" UI.

For /checkout, see the canonical error reference.


Walkthrough — integrating a paid + approval event end-to-end

For events that are paid AND require host approval (Reserve-Before-Decide), the lifecycle has more states than a vanilla paid event. Cobuntu charges the buyer's card at reservation time and holds the money in escrow until the host approves or rejects — guaranteeing the seat is paid-for the moment the host says yes, with no second "now go pay" round-trip.

Here's the full integration with the public API, end to end.

1. The buyer clicks "Reserve" on your event page

Your page calls /checkout exactly the same way it would for a vanilla paid event — the approval-gated semantics are derived from event.requiresApproval on the event GET, and the checkout-session metadata Cobuntu sets behind the scenes flips the sale's payout status to ESCROW instead of ELIGIBLE.

curl -X POST https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/checkout \
  -H "X-API-Key: sk_live_..." \
  -H "X-Cobuntu-Member-Token: $MEMBER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tierId": "tier_…",
    "successUrl": "https://your-site.com/eventos/EXAMPLE_ID?reserve=success&session={CHECKOUT_SESSION_ID}",
    "cancelUrl":  "https://your-site.com/eventos/EXAMPLE_ID?reserve=cancel"
  }'

Redirect to the returned checkoutUrl. The buyer pays. Stripe routes them back to successUrl.

2. Webhook fires event.attendance.application_received and (separately) event.attendance.application_awaiting_review

If your community has a webhook subscriber configured (see /webhooks/events), you'll get these two events shortly after the Stripe webhook lands. The first targets the buyer ("we received your application"); the second targets the host ("a new application needs review"). Cobuntu's own automation flows turn them into emails — your integration can subscribe to them too if you want to fire custom side-effects.

At this point the attendance row exists with status: 'PENDING' and the sale row has payoutStatus: 'ESCROW'.

3. On a return visit, your page calls /attendance-status

Now the buyer comes back to your event page. Show "Your application is pending review" — and let them withdraw it if they want to.

curl https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/attendance-status \
  -H "X-API-Key: pk_live_..." \
  -H "X-Cobuntu-Member-Token: $MEMBER_TOKEN"

Response: { attendanceId, status: 'PENDING', cancellationReason: null, lookupMode: 'member' }.

If status === 'PENDING', render two CTAs:

  • Wait for review — no action needed.
  • Withdraw and refund — calls /attendees/me/cancel-pending (next step).

4. (Optional) The buyer withdraws before the host decides

curl -X POST https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/attendees/me/cancel-pending \
  -H "X-API-Key: sk_live_..." \
  -H "X-Cobuntu-Member-Token: $MEMBER_TOKEN" \
  -d '{}'

Response: { cancelled: true, refundIssued: true, refundAmountCents: 4500, refundCurrency: "EUR", stripeFeeCents: 250 }.

Surface a confirmation toast: "Withdrawn. You'll receive €45 back (€2.50 in processing fees)." The seat is now free; the attendance row is CANCELLED.

5. The host approves (or rejects) inside Cobuntu's admin

This step happens outside your integration — Cobuntu's admin is the host's surface. When the host clicks Approve:

  • The sale's payoutStatus flips from ESCROW to ELIGIBLE. Money is released to the host's next payout cycle.
  • The attendance row stays APPROVED.
  • Webhook fires event.attendance.approved with the full attendance details — listen on your end if you want to fire your own confirmation email.

When the host rejects:

  • The sale is automatically refunded net of Stripe fees (the host pays the loss for rejecting after collecting payment — see T&Cs §7.6).
  • The attendance row flips to REJECTED.
  • Webhook fires event.attendance.rejected.

6. After approval, the buyer can still self-refund (within the 7-day window)

Same endpoint a vanilla paid event uses:

curl -X POST https://api.cobuntu.com/api/v1/communities/my-community/events/EXAMPLE_ID/sales/me/refund \
  -H "X-API-Key: sk_live_..." \
  -H "X-Cobuntu-Member-Token: $MEMBER_TOKEN" \
  -d '{ "saleId": "sale_…", "reason": "Plans changed" }'

Quick state diagram

                ┌─ /checkout ─►  PENDING  + ESCROW
                │                   │
                │       ┌───────────┼───────────┬─────────────────┐
                │       ▼           ▼           ▼                 ▼
                │  host approves  host rejects  buyer cancels    (host decides)
                │       │           │           │                 │
                │       ▼           ▼           ▼                 ▼
                │  APPROVED      REJECTED   CANCELLED         (approves/rejects)
                │  + ELIGIBLE    + REFUNDED + REFUNDED
                │                  (net fees)  (net fees)
                │       │
                │       ▼
                │  buyer self-refund within 7d  →  CANCELLED + REFUNDED

                └─ Cobuntu admin owns approve/reject. Your integration owns reserve + buyer self-cancel.