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:
| Param | Type | Description |
|---|---|---|
communityTag | string | Your community's tag (e.g. your-community, acme). |
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Page size. Default 20, max 100. |
offset | integer | 0 | Zero-based offset for pagination. Use either offset or cursor — not both. |
featured | boolean | If true, filter to the single featured event for this community. | |
upcoming | boolean | If true, filter to events with startDate >= now. | |
cursor | string | Opaque 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,
}
]| Field | Type | Description |
|---|---|---|
id | string | Stable event identifier (uuid). |
slug | string | URL-safe identifier, unique per community. Use this for deep-links. |
name | string | Display name shown on listings and detail pages. |
description | string (nullable) | Long-form description (may contain markdown). |
startDate | string (date-time) | ISO 8601 start timestamp. UTC. Convert to your visitor's locale client-side. |
endDate | string (date-time) (nullable) | ISO 8601 end timestamp. Null for open-ended events. |
timezone | string (IANA) | The event's IANA timezone (e.g. Europe/Lisbon). Use it to render startDate/endDate/agenda times in local time. |
physicalLocation | string (nullable) | Physical venue — name and/or address (e.g. "Casa Tônico"). Null for online-only or TBD. |
onlineUrl | string (nullable) | Join / details URL for online events. Null for in-person events. |
bannerUrl | string (nullable) | Hero image URL (or null). Recommended display: 16:9. |
accessibility | string (enum) | PUBLIC or MEMBERS_ONLY. MEMBERS_ONLY events return 404 on the public API for non-members. |
tiers | array | Active price tiers for this event. Empty when the event isn't yet on sale. |
agenda | array | Ordered agenda / itinerary items (see Agenda item). Empty [] when no agenda has been set. |
featured | boolean | True 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:
| Param | Type | Description |
|---|---|---|
communityTag | string | Your community's tag (e.g. your-community, acme). |
slug | string | URL 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,
}| Field | Type | Description |
|---|---|---|
id | string | Stable event identifier (uuid). |
slug | string | URL-safe identifier, unique per community. Use this for deep-links. |
name | string | Display name shown on listings and detail pages. |
description | string (nullable) | Long-form description (may contain markdown). |
startDate | string (date-time) | ISO 8601 start timestamp. UTC. Convert to your visitor's locale client-side. |
endDate | string (date-time) (nullable) | ISO 8601 end timestamp. Null for open-ended events. |
timezone | string (IANA) | The event's IANA timezone (e.g. Europe/Lisbon). Use it to render startDate/endDate/agenda times in the event's local time. |
physicalLocation | string (nullable) | Physical venue — name and/or address (e.g. "Casa Tônico"). Null for online-only or TBD. |
onlineUrl | string (nullable) | Join / details URL for online events. Null for in-person events. |
bannerUrl | string (nullable) | Hero image URL (or null). Recommended display: 16:9. |
accessibility | string (enum) | PUBLIC or MEMBERS_ONLY. MEMBERS_ONLY events return 404 on the public API for non-members. |
tiers | array | Active price tiers for this event. Empty when the event isn't yet on sale. |
agenda | array | Ordered agenda / itinerary items (see Agenda item). Empty [] when no agenda has been set. |
featured | boolean | True 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
}| Field | Type | Description |
|---|---|---|
title | string | Agenda step label (e.g. "Palestra", "Jantar"). |
description | string (nullable) | Optional longer detail for the step. |
startTime | string (date-time) | ISO 8601 (UTC) start instant. Render in the event's timezone. |
endTime | string (date-time) | ISO 8601 (UTC) end instant. |
order | integer | Zero-based display order. |
GET /communities/{communityTag}/events/{slug}/tiers — List tiers for an event
Scope: READ_PUBLIC
Path parameters:
| Param | Type | Description |
|---|---|---|
communityTag | string | Your community's tag (e.g. your-community, acme). |
slug | string | URL 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,
}
]| Field | Type | Description |
|---|---|---|
id | string | Stable tier identifier (uuid). |
name | string | Display name (e.g. Early bird, VIP). |
priceCents | integer | Price in the smallest currency unit (cents for EUR/USD/GBP, no decimals for JPY). |
currency | string | ISO 4217 code. |
capacity | integer (nullable) | Max attendees who can buy this tier. Null = unlimited. |
sold | integer | Attendees already on this tier. Read-only. |
isActive | boolean | Whether the tier accepts new RSVPs. |
hasForm | boolean | True 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:
| Param | Type | Description |
|---|---|---|
communityTag | string | Your community's tag (e.g. your-community, acme). |
slug | string | URL 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"
}| Field | Type | Description |
|---|---|---|
email | string (email) | Required. Attendee email. Used for confirmation + reminder emails. Ignored when an authenticated member token identifies the buyer. |
formAnswers | object (optional) | Answers to the tier's form, if it has one. |
donation | object (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). |
successUrl | string (required iff donation) | Absolute URL to redirect to after a successful donation. Supports Stripe's {CHECKOUT_SESSION_ID} placeholder. |
cancelUrl | string (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"
}| Field | Type | Description |
|---|---|---|
attendanceId | string | Stable id for the new attendance. Idempotent on (eventId, email) — re-submitting returns the existing id. |
status | string | APPROVED for instant-registration events, PENDING for approval-gated events. |
Response — donation in body (200):
{
"checkoutUrl": "https://checkout.stripe.com/c/pay/…",
"sessionId": "cs_…"
}| Field | Type | Description |
|---|---|---|
checkoutUrl | string | Redirect 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. |
sessionId | string | Stripe 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
}| Field | Type | Description |
|---|---|---|
tierId | string | Required. The paid tier to purchase. Must currently be on sale (else 409 TIER_NOT_ON_SALE). |
quantity | integer | Tickets to buy. Defaults to 1; capped at 10 per session. |
customer | object (optional) | { email, name }. Ignored when the caller is an identified member; Stripe also collects email at the session. |
successUrl | string | Required. Absolute URL to redirect to on success. Supports Stripe's {CHECKOUT_SESSION_ID} placeholder. |
cancelUrl | string | Required. Absolute URL to redirect to on cancellation. |
formAnswers | object (optional) | Answers to the tier's form, if it has one. |
donation | object (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_…"
}| Field | Type | Description |
|---|---|---|
checkoutUrl | string | Redirect the buyer here to complete payment. |
sessionId | string | Stripe 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"
}| Field | Type | Description |
|---|---|---|
donation | object | Required. { amount } — positive integer in the donation currency's smallest unit. Validated against event.donationConfig. |
customer | object (optional) | { email }. Required for anonymous donors; ignored when an identified member is forwarded via the auth headers below. |
successUrl | string | Required. Absolute URL to redirect to on success. Supports Stripe's {CHECKOUT_SESSION_ID} placeholder. |
cancelUrl | string | Required. 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"
}| Field | Type | Description |
|---|---|---|
total | integer | Sum of donation amounts in the currency's smallest unit. Returns 0 when no donations exist yet. |
count | integer | Number of donation rows attached to the event. |
currency | string | null | ISO 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:
| Param | Type | Description |
|---|---|---|
communityTag | string | Your community's tag. |
tierId | string | The 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 }| Field | Type | Description |
|---|---|---|
tierId | string | Echo of the path parameter. |
formData | object | null | The opaque schema blob. null means no form is configured for this tier — your checkout page can skip rendering fields entirely. |
formData.fields[] | array | The form fields, in display order. Render in the order returned. |
formData.fields[].id | string | Stable field id. Use this as the key when submitting formAnswers to /rsvp or /checkout. |
formData.fields[].type | string | One of SHORT_TEXT, LONG_TEXT, EMAIL, PHONE, NUMBER, SELECT, CHECKBOX. Drives input rendering on your end. |
formData.fields[].label | string | Human-readable label. Shown to buyers; echoed back in the host's attendee CSV export. |
formData.fields[].required | boolean (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 type | How they identify | Where the token lives |
|---|---|---|
| Member (logged into the community on a customer apex) | X-Cobuntu-Auth-Token header | Read from the cobuntu_auth_<tag> cookie set after /login |
| Member (post-SSO handshake) | X-Cobuntu-Member-Token header | URL fragment #cobuntu_session=… captured by your app, persisted in storage |
| Guest (email-only, no Cobuntu account) | token in request body | Signed 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-TokenorX-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:
| Param | Type | Description |
|---|---|---|
communityTag | string | Your community's tag. |
slug | string | The event slug. |
Query parameters:
| Param | Type | Description |
|---|---|---|
email | string (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"
}| Field | Type | Description |
|---|---|---|
attendanceId | string | null | The attendance row id. null when no row exists for this caller — render "Register" UI. |
status | string | null | One of APPROVED, PENDING, REJECTED, CANCELLED. null when no row. |
cancellationReason | string | null | When status === 'CANCELLED', the cause: USER_REQUEST, ADMIN_REMOVAL, or PAYMENT_TIMEOUT. Use it to surface cause-specific copy in your UI. |
lookupMode | string | "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
}| Field | Type | Description |
|---|---|---|
token | string (guest-only) | Signed MagicLink token emailed at registration. Required for guest leaves. See Magic-link tokens for guests. |
forceWithoutRefund | boolean (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
}| Field | Type | Description |
|---|---|---|
message | string | Human-readable confirmation. Surface as-is or replace with your own copy. |
wasPaidAttendee | boolean | true if the attendance was a paid ticket. |
refundProcessed | boolean | true when the Stripe refund went through. |
refundAmount | integer | null | Refund amount in the currency's smallest unit (e.g. cents). Net of Stripe processing fees per T&Cs §7.6. |
Errors:
| Status | Code | Meaning |
|---|---|---|
400 | REFUND_NOT_ELIGIBLE | Buyer 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. |
401 | — | No member token AND no body token. |
403 | — | Guest token doesn't match this event. |
500 | REFUND_FAILED | Stripe 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
}| Field | Type | Description |
|---|---|---|
cancelled | boolean | Always true on a 200 response. |
refundIssued | boolean | true when Stripe accepted the refund. |
refundAmountCents | integer | null | What the buyer received back, in smallest unit. |
refundCurrency | string | null | ISO 4217 code (e.g. EUR). |
stripeFeeCents | integer | null | Fee Stripe kept (your buyer absorbed). Surface this in your confirmation toast: "You'll receive €45 back (€2.50 in processing fees)". |
Errors:
| Status | Meaning |
|---|---|
401 | No member token forwarded. |
404 | No PENDING reservation found for this buyer + event. (E.g. the host already approved them — use /leave instead.) |
409 | The 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 state | Endpoint |
|---|---|
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 refund | This endpoint (with amount) |
Request body:
{
"saleId": "…",
"reason": "…",
"amount": 25.00,
"refundType": "BUYER_REQUEST"
}| Field | Type | Description |
|---|---|---|
saleId | string | Required. 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. |
reason | string | Required. Free-text reason. Stored on the refund row for the host's audit log. |
amount | number (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. |
refundType | string (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:
| Status | Meaning |
|---|---|
400 | Missing saleId / reason, invalid amount, or refund not eligible (outside 7-day window, sale already refunded, etc.). |
401 | No member token forwarded. |
403 | The authenticated buyer doesn't own this sale. |
404 | saleId 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:
| Type | What it carries | Emitted by |
|---|---|---|
event-cancel | attendanceId + guestEmail directly | Free-RSVP receipt + R-B-D reservation receipt |
sale-manage | saleId + 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:
- Your email template (set in admin) includes a deep link to your own cancellation page:
https://your-site.com/events/{slug}/cancel?token={token}. - Your page extracts the
tokenquery parameter and POSTs it to/leave(or/attendees/me/cancel-pendingif the buyer isPENDING). - 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? }):
| Status | Code (or shape) | Cause | Resolution |
|---|---|---|---|
400 | PAID_TIER_REQUIRES_CHECKOUT | Tier resolved to a price > 0 — /rsvp is free-only. | Switch to /checkout. |
400 | INVALID_DONATION_AMOUNT | donation.amount is not a positive integer, or below Stripe's 50-smallest-unit minimum. | Render an input validator that mirrors the constraint. |
400 | DONATION_REQUIRES_REDIRECT_URLS | donation present without successUrl / cancelUrl, or URLs aren't absolute. | Include both URLs starting with http(s)://. |
400 | error: email is required | Body missing email AND no member token forwarded. | Either collect an email or forward the buyer's token. |
400 | error: Event has no tiers | The event hasn't been configured with at least one tier. | Surface "registration not open yet"; the host needs to publish a tier. |
400 | error: Donations are not configured… | donation body field present, but donationConfig.enabled === false on this event. | Check donationConfig on the event GET first. |
400 | error: missing form answers | The tier has a required form and formAnswers is missing or incomplete. | Call /tiers/{tierId}/form first and render the required fields. |
404 | error: Event not found | Slug 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. |
409 | error contains already registered / already has | Idempotency hit — this email or user already has an attendance for this event. | Surface "you're already registered" + (optionally) call /attendance-status to confirm. |
409 | error contains full / capacity | All 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
payoutStatusflips fromESCROWtoELIGIBLE. Money is released to the host's next payout cycle. - The attendance row stays
APPROVED. - Webhook fires
event.attendance.approvedwith 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.