Webhooks
Event catalog

Event catalog

Cobuntu emits 49 webhook events across 8 domains. Subscribe to whichever ones you need in admin → Integrations → Webhooks.

What we expose vs. what we keep silent

Cobuntu is built to be the invisible OS for your community. Setup-side signals — Stripe Connect verification, payout-rail health, account onboarding state — are handled by us on your behalf and are not exposed as customer webhooks. The events catalogued here cover what's happening inside your community: members, events, marketplace, sales, payouts. If a signal feels like platform plumbing, that's by design — those are ours to reconcile, not yours to subscribe to.

Every event payload arrives with the same envelope:

{
  "event": "member.approved",
  "deliveryId": "del_8f9a2b3c4d5e6f7g",
  "createdAt": "2026-05-26T15:33:20Z",
  "communityTag": "my-community",
  "data": { /* per-event fields below */ }
}

Delivery is at-least-once, HMAC-signed (see Verify signatures), and retried with exponential backoff (see Retry & delivery).

Quick index

DomainEventsJump
Memberships8member.requested, member.invited, member.approved, member.rejected, form.submitted, member.kicked, member.banned, application.withdrawn
Articles2article.published, article.deleted
Events9event.published, event.cancelled, event.attendance.application_received, event.attendance.application_awaiting_review, event.attendance.registered, event.attendance.approved, event.attendance.payment_pending, event.attendance.rejected, event.attendance.cancelled, event.reminder.sent
Marketplace (listings)5listing.requested, listing.approved, listing.rejected, listing.deactivated, listing.withdrawn
Sales (orders + refunds)10sale.completed, refund.completed, refund.issued, event.ticket.purchased, event.ticket.refunded, product.purchased, product.refunded, donation.received, donation.refunded, dispute.created
Subscriptions9subscription.activated, subscription.renewed, subscription.payment_failed, subscription.cancelled, subscription.cancelled_by_admin, subscription.expired, subscription.purchase_receipt, tier.changed, member.discount.ended
Payouts & finance5payout.confirmed, payout.failed, commission.held, stripe.account.requires_verification, stripe.account.payouts_disabled
Communications2broadcast.sent, digest.scheduled

Memberships

member.requested — Membership requested

Someone submitted a membership form (Tally / JotForm / in-app form / CSV import). Fires for every new request, regardless of community accessibility.

When it fires. A prospective member submits a membership form — from an in-app form, a Tally / JotForm embed, the marketing landing page, or a bulk CSV import. Fires once per submission. For an OPEN community the request is auto-approved and member.approved fires immediately afterward; for review-required communities the request is left PENDING.

Example payload (data field):

{
  requestId: "f1c2a8d3-4e5b-4c7a-9f1e-8d2b6a5c3f01",
  segmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  segmentName: "Founding Members",
  email: "alice@example.com",
  name: "Alice Souza",
  source: "tally",
  answers: { company: "Acme", referral: "Friend", whyJoin: "Looking for like-minded founders." }
}

Payload schema:

FieldTypeNullableDescription
requestIduuidStable identifier for this membership request.
segmentIduuidSegment the request targets; null when applying without a specific tier.
segmentNamestringHuman-readable segment name, mirrors segmentId.
emailstringEmail submitted on the form.
namestringDisplay name; null when the form didn't collect one.
sourcestringWhere the request came from — e.g. tally, jotform, landing_page, csv_import, admin_added.
answersobjectRaw form answers keyed by field name. Shape varies per community form.

Related: member.approved, member.rejected, application.withdrawn

member.invited — Member invited

An admin sent a personal invitation to join the community (or a specific tier / role group within it).

When it fires. An admin clicks Send in Members > Invite Members. Fires once per recipient email. The invite carries a token; the recipient clicks the link, lands on /join, and accepts → member.approved fires next.

Example payload (data field):

{
  email: "alice@example.com",
  inviterName: "Diogo Cesar",
  inviterAvatar: "https://storage.googleapis.com/.../diogo.png",
  customMessage: "Hey Alice — I'd love for you to join us.",
  acceptUrl: "https://my-community.cobuntu.com/join?invite=t_abc123…",
  segmentName: "Founding Members",
  segmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  communityDescription: "A community for travellers who want to experience Lisbon like a local."
}

Payload schema:

FieldTypeNullableDescription
emailstringInvitee's email address.
inviterNamestringDisplay name of the admin who sent the invitation.
inviterAvatarstringProfile-image URL of the inviter; null when no avatar set.
customMessagestringOptional personal message the admin attached to the invitation.
acceptUrlstringTokenised URL the invitee opens to accept. Points at the community subdomain's /join (or admin.cobuntu.com/join for role-gated invites).
segmentNamestringHuman-readable name of the tier the invite targets; null when no tier was specified.
segmentIduuidTier (segment) the invite targets; null when none was specified.
communityDescriptionstringShort description of the community, used to give the recipient context.

Related: member.approved, member.rejected

member.approved — Membership approved

A membership request was approved (manually by an admin OR auto-approved for OPEN communities).

When it fires. A pending membership request transitions to APPROVED — either an admin clicked Approve in the admin app, or the community is OPEN and the request was auto-approved immediately after member.requested. The user has full member access from this point.

Example payload (data field):

{
  requestId: "f1c2a8d3-4e5b-4c7a-9f1e-8d2b6a5c3f01",
  email: "alice@example.com",
  name: "Alice Souza"
}

Payload schema:

FieldTypeNullableDescription
requestIduuidMatches the requestId from the original member.requested event.
emailstringApproved member's email.
namestringDisplay name; null when unknown.

Related: member.requested, member.rejected

member.rejected — Membership rejected

A membership request was rejected by an admin.

When it fires. An admin reviewed a pending membership request and rejected it. The applicant does not get member access; they can re-apply unless explicitly banned.

Example payload (data field):

{
  requestId: "f1c2a8d3-4e5b-4c7a-9f1e-8d2b6a5c3f01",
  email: "alice@example.com",
  name: "Alice Souza"
}

Payload schema:

FieldTypeNullableDescription
requestIduuidMatches the requestId from the original member.requested event.
emailstringRejected applicant's email.
namestringDisplay name; null when unknown.

Related: member.requested, member.approved

form.submitted — Form submitted (deprecated)

DEPRECATED — alias for member.requested, fired alongside it for back-compat. New integrations should subscribe to member.requested instead.

When it fires. Fires alongside every member.requested event for backwards compatibility with older integrations that pre-date the rename. New integrations should subscribe to member.requested directly — this alias will be removed in a future major version.

Example payload (data field):

{
  segmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  segmentName: "Founding Members",
  email: "alice@example.com",
  name: "Alice Souza",
  source: "tally",
  answers: { company: "Acme", referral: "Friend" }
}

Payload schema:

FieldTypeNullableDescription
segmentIduuidSegment the form was scoped to; null when unscoped.
segmentNamestringHuman-readable segment name.
emailstringSubmitted email.
namestringDisplay name.
sourcestringForm provider — tally, jotform, landing_page.
answersobjectRaw form answers keyed by field name.

Related: member.requested

member.kicked — Member kicked

An admin removed a member from the community. The member can re-apply unless also banned.

When it fires. An admin removed an active member from the community (kicked, not banned). The user loses member access immediately but can submit a fresh membership request later.

Example payload (data field):

{
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  usertag: "alice",
  email: "alice@example.com",
  name: "Alice Souza",
  kickedBy: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a"
}

Payload schema:

FieldTypeNullableDescription
userIduuidThe kicked member's user id.
usertagstringTheir @usertag (handle).
emailstringEmail at the time of kick.
namestringDisplay name; null when unknown.
kickedByuuidThe admin user id who performed the kick.

Related: member.banned

member.banned — Member banned

An admin banned a member — irreversible. The member is removed and prevented from re-applying or accepting a future invite.

When it fires. An admin banned a member — a stronger version of kick. The user loses access immediately and is blocked from re-applying or accepting future invites to this community. Bans are not auto-revoked.

Example payload (data field):

{
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  usertag: "alice",
  email: "alice@example.com",
  name: "Alice Souza",
  bannedBy: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a",
  reason: "Repeated harassment after multiple warnings."
}

Payload schema:

FieldTypeNullableDescription
userIduuidThe banned member's user id.
usertagstringTheir @usertag (handle).
emailstringEmail at the time of ban.
namestringDisplay name; null when unknown.
bannedByuuidThe admin user id who issued the ban.
reasonstringFree-text reason recorded by the admin; null when not provided.

Related: member.kicked

application.withdrawn — Application withdrawn

A user withdrew their own pending membership request before it was reviewed.

When it fires. An applicant cancelled their own pending membership request before any admin reviewed it. The request transitions to WITHDRAWN and is removed from the admin review queue.

Example payload (data field):

{
  requestId: "f1c2a8d3-4e5b-4c7a-9f1e-8d2b6a5c3f01",
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  name: "Alice Souza",
  segmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e"
}

Payload schema:

FieldTypeNullableDescription
requestIduuidMatches the original member.requested event.
userIduuidUser id if the applicant had an account; null for anonymous form submissions.
emailstringApplicant email.
namestringDisplay name; null when unknown.
segmentIduuidSegment the application targeted; null when unscoped.

Related: member.requested


Articles

article.published — Article published

An article was published or updated to PUBLISHED status.

When it fires. An admin-authored article transitions to PUBLISHED status. Fires both when an article is published for the first time and when its content is updated while already published — consumers should treat it as an upsert, not a first-publish signal.

Example payload (data field):

{
  id: "a3f9c2e0-7e1a-4d2f-9b8a-1c5b3a8f6e92",
  title: "The Quiet Side of Lisbon",
  slug: "the-quiet-side-of-lisbon",
  excerpt: "Beyond the tourist trails — eight hidden corners locals love.",
  bannerUrl: "https://cdn.cobuntu.com/articles/lisbon-banner.jpg",
  category: "City Guide",
  publishedAt: "2026-05-18T09:00:00Z"
}

Payload schema:

FieldTypeNullableDescription
iduuidStable article identifier.
titlestringArticle title at the time of publish.
slugstringURL-safe slug used in the article's public URL.
excerptstringShort summary intended for cards and previews.
bannerUrlurlHero image; null when no banner was uploaded.
categorystringEditorial category; null when uncategorized.
publishedAtiso-8601When the article entered PUBLISHED status.

Related: article.deleted

article.deleted — Article deleted

An article was deleted.

When it fires. An article was permanently deleted by an admin. Fires once at deletion time and the article will not subsequently reappear under the same id.

Example payload (data field):

{
  id: "a3f9c2e0-7e1a-4d2f-9b8a-1c5b3a8f6e92",
  title: "The Quiet Side of Lisbon",
  slug: "the-quiet-side-of-lisbon"
}

Payload schema:

FieldTypeNullableDescription
iduuidThe article's stable identifier — useful for cleaning up external mirrors.
titlestringArticle title at the time of deletion (snapshot for logging).
slugstringSlug that was previously in use; safe to reuse from this point.

Related: article.published


Events

event.published — Event published

An event went from DRAFT to LIVE — listings became visible.

When it fires. A community event transitions from DRAFT to LIVE status — its listing becomes visible to the relevant audience (public, members-only, or tier-scoped). Fires once per publish transition; editing details after publish does not re-fire.

Example payload (data field):

{
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  slug: "summer-rooftop-meetup",
  name: "Summer Rooftop Meetup",
  startDate: "2026-07-12T18:00:00Z",
  endDate: "2026-07-12T22:00:00Z",
  accessibility: "PUBLIC"
}

Payload schema:

FieldTypeNullableDescription
eventIduuidStable event identifier.
slugstringURL-safe slug used in the event's public URL.
namestringEvent title.
startDateiso-8601Event start; null for date-less events.
endDateiso-8601Event end; null when not set or when same-day with implicit length.
accessibility`PUBLICMEMBERS_ONLY`

Related: event.cancelled

event.cancelled — Event cancelled

An event was unpublished or deleted. Auto-refunds for escrowed paid attendees fire as separate refund.completed events.

When it fires. A LIVE event was unpublished or deleted. The listing is removed and registered attendees can no longer find it. If the event had paid attendees still in escrow, refunds are auto-issued and surface as separate refund.completed / refund.issued / event.ticket.refunded events per attendee — handle those for refund-specific actions.

Example payload (data field):

{
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  slug: "summer-rooftop-meetup",
  name: "Summer Rooftop Meetup",
  action: "unpublished",
  attendeeCount: 42
}

Payload schema:

FieldTypeNullableDescription
eventIduuidMatches the eventId from event.published.
slugstringSlug that was in use.
namestringEvent title.
action`unpublisheddeleted`
attendeeCountnumberNumber of registered attendees at cancellation time — useful for sizing follow-up comms.

Related: event.published, refund.completed

event.attendance.application_received — Application received (applicant-facing)

A member applied to a requires-approval event. Fires once per community-listing on PENDING attendance creation.

Default automation. [System] Event application received sends the applicant a "we received your application" email. Disable it in admin → Automations and consume this webhook to send your own confirmation.

When it fires. A member submits an event registration on an event with requiresApproval=true — the attendance lands in PENDING. Fires for both auth-user and guest-email applications. Does NOT fire for free no-approval events (those go straight to APPROVED + event.attendance.registered).

Example payload (data field):

{
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  attendanceId: "4a7d1c3b-5e2f-4b8a-9c1d-6f3a8b2e5d7c",
  email: "alice@example.com",
  name: "Alice Souza",
  eventName: "Founders' Dinner — June",
  eventStartDate: "2026-06-18T17:00:00Z",
  eventLocation: "Plumas & Flores, Porto",
  eventUrl: "https://my-community.cobuntu.com/c/my-community/events/founders-dinner-june",
  isGuest: false
}

Payload schema:

FieldTypeNullableDescription
eventIduuidThe event the member applied to.
attendanceIduuidThe PENDING attendance row.
emailstringApplicant email (user or guest).
namestringDisplay name; null when guest with no name.
eventNamestringEvent title at apply-time.
eventStartDateiso-datetimeEvent start (ISO 8601).
eventLocationstringPhysical location or online URL.
eventUrlurlCommunity-app event detail page.
isGuestbooleanTrue when the applicant did not have a Cobuntu account.

Related: event.attendance.application_awaiting_review, event.attendance.approved, event.attendance.rejected

event.attendance.application_awaiting_review — Application awaiting review (organiser-facing)

A new application is waiting for host or community-leader review. Fires once per community-listing of the event.

Default automation. [System] Event application awaiting review emails the event CREATOR host + community members with EVENTS_MANAGE_LISTINGS. Disable it in admin → Automations and iterate notifiedRecipients in the webhook payload to fan out from your own system.

When it fires. A member applied to attend a requires-approval event. Pairs with event.attendance.application_received (applicant-facing) at the same moment.

Example payload (data field):

{
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  attendanceId: "4a7d1c3b-5e2f-4b8a-9c1d-6f3a8b2e5d7c",
  applicantEmail: "alice@example.com",
  applicantName: "Alice Souza",
  applicantUsertag: "alice",
  isGuest: false,
  eventName: "Founders' Dinner — June",
  pendingApprovalCount: 3,
  reviewUrl: "https://admin.cobuntu.com/my-community/events/founders-dinner-june",
  notifiedRecipients: [
    { email: "host@example.com", name: "Álvaro de Mello", role: "CREATOR" },
    { email: "leader@example.com", name: "Art Linkov", role: "EVENTS_MANAGE_LISTINGS" }
  ]
}

Payload schema:

FieldTypeNullableDescription
eventIduuidThe event the member applied to.
attendanceIduuidThe PENDING attendance row awaiting review.
applicantEmailstringThe member who applied.
applicantNamestringApplicant's display name; null for unnamed guests.
applicantUsertagstringApplicant's usertag; null for guests.
isGuestbooleanTrue when the applicant did not have a Cobuntu account.
eventNamestringEvent title at apply-time.
pendingApprovalCountnumberTotal PENDING attendances on this event right now (this one included).
reviewUrlurlAdmin event manage page deep-link to the Pending tab.
notifiedRecipientsarray<{email, name, role}>Emails that Cobuntu's [System] automation would notify: event CREATOR host + community leaders with EVENTS_MANAGE_LISTINGS. Use this list to drive your own fan-out.

Related: event.attendance.application_received, event.attendance.approved, event.attendance.rejected

event.attendance.registered — Attendance registered

A person registered for an event — free, paid (after Stripe settlement), or an approved pending application.

When it fires. Someone successfully became an attendee on an event. Fires for free events on form submit, for paid events after Stripe settlement, and for review-required events when an admin approves a pending application. Fires once per attendance row.

Example payload (data field):

{
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  attendanceId: "f9e1c3a7-2d4b-4a8c-9e6f-1d3a5b7c9e2f",
  tierId: null,
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  name: "Alice Souza",
  status: "APPROVED",
  isGuest: false,
  source: "free_rsvp"
}

Payload schema:

FieldTypeNullableDescription
eventIduuidThe event being registered for.
attendanceIduuidStable identifier for this attendance row.
tierIduuidTicket tier; null for free-RSVP events without tiers.
userIduuidLogged-in user id; null for guest sign-ups (magic-link).
emailstringAttendee email.
namestringDisplay name; null when not provided.
statusAPPROVED | PENDINGAlways APPROVED for this event; PENDING means the registration entered the queue and this event has not yet fired.
isGuestbooleanTrue for magic-link guests without an account.
sourcefree_rsvp | paid_sale | invitation | admin_addedHow the registration was created.

Related: event.attendance.approved, event.attendance.cancelled, event.ticket.purchased

event.attendance.approved — Attendance approved

A host approved a PENDING attendance — the applicant is now confirmed for the event.

When it fires. A host approved a pending application from the event's review queue. The applicant transitions to confirmed; downstream send a confirmation email or unlock event resources.

Example payload (data field):

{
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  attendanceId: "f9e1c3a7-2d4b-4a8c-9e6f-1d3a5b7c9e2f",
  email: "alice@example.com",
  name: "Alice Souza",
  reviewedBy: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a"
}

Payload schema:

FieldTypeNullableDescription
eventIduuidThe event being attended.
attendanceIduuidMatches the attendance row.
emailstringAttendee email.
namestringDisplay name; null when unknown.
reviewedByuuidThe host user id who approved.

Related: event.attendance.registered, event.attendance.rejected

event.attendance.payment_pending — Attendee approved — payment pending

Host approved a PENDING attendance on a paid + requires-approval event. The attendee has 48 hours to complete Stripe Checkout via the Pay-now magic link, or the row flips to CANCELLED.

Default automation. [System] Event payment pending emails the applicant the Pay-now link. Disable it in admin → Automations and use paymentUrl from this webhook to email the applicant from your own system — it's the same single-use signed link Cobuntu would have sent.

When it fires. A host approved a PENDING attendance AND the registrant's tier costs money AND the registrant is not a host of the event. Holds the seat for 48 hours. Does NOT fire for the host carve-out path — when a host of the event applies and gets approved, the row goes straight to APPROVED and event.attendance.approved fires instead.

Example payload (data field):

{
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  attendanceId: "4a7d1c3b-5e2f-4b8a-9c1d-6f3a8b2e5d7c",
  email: "alice@example.com",
  name: "Alice Souza",
  reviewedBy: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a",
  eventName: "Founders' Dinner — June",
  eventUrl: "https://my-community.cobuntu.com/c/my-community/events/founders-dinner-june",
  paymentUrl: "https://my-community.cobuntu.com/checkout/pay-event?token=...",
  paymentExpiresAt: "2026-06-05T15:00:00Z",
  tierName: "Standard ticket",
  tierPriceCents: 2400,
  tierCurrency: "EUR"
}

Payload schema:

FieldTypeNullableDescription
eventIduuidThe event being attended.
attendanceIduuidMatches the attendance row.
emailstringAttendee email.
namestringDisplay name; null when unknown.
reviewedByuuidThe host user id who approved.
eventNamestringEvent title.
eventUrlurlCommunity-app event detail page.
paymentUrlurlMagic-link to complete Stripe Checkout. Single-use, signed, expires with paymentExpiresAt.
paymentExpiresAtiso-datetimeDeadline for completing payment. After this, the row flips to CANCELLED.
tierNamestringTier the attendee was approved for.
tierPriceCentsnumberTier price in minor currency units (cents).
tierCurrencystringISO 4217 currency code.

Related: event.attendance.approved, event.attendance.registered, event.attendance.cancelled

event.attendance.rejected — Attendance rejected

A host rejected a PENDING attendance. If the registration was paid, the refund is auto-issued and arrives as a separate refund.completed event.

When it fires. A host rejected a pending application. For paid events the rejection triggers an automatic refund, surfaced as a separate refund.completed / event.ticket.refunded pair.

Example payload (data field):

{
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  attendanceId: "f9e1c3a7-2d4b-4a8c-9e6f-1d3a5b7c9e2f",
  email: "alice@example.com",
  name: "Alice Souza",
  reviewedBy: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a",
  reason: "Sold out — added to the waitlist."
}

Payload schema:

FieldTypeNullableDescription
eventIduuidThe event being attended.
attendanceIduuidMatches the attendance row.
emailstringApplicant email.
namestringDisplay name; null when unknown.
reviewedByuuidThe host user id who rejected.
reasonstringOptional free-text reason recorded by the host.

Related: event.attendance.approved, refund.completed

event.attendance.cancelled — Attendance cancelled

An attendance row exited — self-cancel, admin removal, or 48h payment-window expired without payment.

Default automation. [System] Event attendance cancelled emails the attendee. Branch on cancellationReason in the payload to send different copy per cause. Disable it in admin → Automations and use the webhook directly if your messaging needs are richer than the default template.

When it fires. An attendance row entered the terminal CANCELLED state. Causes are carried in cancellationReason: USER_REQUEST (member clicked cancel / used magic-link unsubscribe), ADMIN_REMOVAL (host removed them post-approval), PAYMENT_TIMEOUT (PENDING_PAYMENT row sat unpaid for 48h and the expire-sweep flipped it). For paid registrations with a sale on file, a refund is processed and refund.completed + event.ticket.refunded fire separately.

Example payload (data field):

{
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  attendanceId: "f9e1c3a7-2d4b-4a8c-9e6f-1d3a5b7c9e2f",
  email: "alice@example.com",
  name: "Alice Souza",
  wasPaid: true,
  refundAmount: 1500,
  cancellationReason: "USER_REQUEST",
  eventName: "Founders' Dinner — June"
}

Payload schema:

FieldTypeNullableDescription
eventIduuidThe event being left.
attendanceIduuidThe attendance row that was cancelled.
emailstringAttendee email.
namestringDisplay name; null when unknown.
wasPaidbooleanTrue if the original registration was paid (not free RSVP or invitation).
refundAmountnumberRefund amount in minor currency units (cents). Null for free cancellations.
cancellationReasonUSER_REQUEST | ADMIN_REMOVAL | PAYMENT_TIMEOUTThe trigger that produced this CANCELLED row. Branch on this to route different downstream automations.
eventNamestringEvent title at cancel-time.

For paid cancellations the refund is dispatched as a separate refund.completed / event.ticket.refunded event with full Stripe context. Don't try to derive accounting from refundAmount here.

Related: event.attendance.registered, event.attendance.payment_pending, refund.completed, event.ticket.refunded

event.reminder.sent — Event reminder sent

A scheduled event reminder went out — one event per recipient × window × channel.

When it fires. The reminder cron dispatched a reminder for an event. Reminders fire on the four standard windows (7 days, 2 days, 1 day, 1 hour before start) over both EMAIL and WHATSAPP channels — communities can disable specific windows in event settings. One event per (recipient × window × channel) tuple.

Example payload (data field):

{
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  attendanceId: "4a7d1c3b-5e2f-4b8a-9c1d-6f3a8b2e5d7c",
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  reminderType: "1d",
  channel: "EMAIL",
  sentAt: "2026-07-11T18:00:00Z",
  eventName: "Summer Rooftop Meetup",
  eventDate: "12 Jul 2026",
  eventTime: "18:00–22:00 WEST",
  eventUrl: "https://acme.cobuntu.com/events/summer-rooftop-meetup",
  calendarUrl: "https://acme.cobuntu.com/events/summer-rooftop-meetup.ics"
}

Payload schema:

FieldTypeNullableDescription
eventIduuidEvent the reminder is for.
attendanceIduuidRecipient's attendance row; null for non-attendee broadcast reminders.
userIduuidRecipient user id; null for guests addressed by email only.
emailstringReminder recipient.
reminderType7d | 2d | 1d | 1hWhich reminder window fired.
channelEMAIL | WHATSAPPChannel the reminder was sent on.
sentAtiso-8601When the reminder was actually dispatched.
eventNamestringEvent title at send time.
eventDatestringPre-formatted localized date string for rendering.
eventTimestringPre-formatted localized time-of-day string.
eventUrlurlPublic URL of the event.
calendarUrlurliCalendar (.ics) link for add-to-calendar.

Related: event.attendance.registered


Marketplace (listings)

listing.requested — Listing submitted for review

A seller submitted a marketplace listing for community review.

When it fires. A seller submitted a new marketplace listing for moderation. The listing enters PENDING_REVIEW status — fires listing.approved or listing.rejected after admin action.

Example payload (data field):

{
  listingId: "e5f1c3a7-2d4b-4c8e-9f6a-1b3d5c8a7e2f",
  communityId: "5ea4e9c6-d3f1-4b2e-8a7d-9c1e4f6a2b8d",
  sellerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  sellerEmail: "alice@example.com",
  title: "Lisbon Photo Guide (PDF)",
  listingType: "PRODUCT",
  price: 2500,
  currency: "eur"
}

Payload schema:

FieldTypeNullableDescription
listingIduuidStable listing identifier.
communityIduuidCommunity the listing was submitted to.
sellerUserIduuidSeller user id.
sellerEmailstringSeller email.
titlestringSubmitted listing title.
listingType`PRODUCTSERVICE`
pricenumberListed price in minor currency units.
currencystringISO 4217 currency code (lowercase).

Related: listing.approved, listing.rejected, listing.withdrawn

listing.approved — Listing approved

A community admin approved a pending marketplace listing.

When it fires. A community admin approved a pending marketplace listing. The listing transitions to ACTIVE and becomes purchasable.

Example payload (data field):

{
  listingId: "e5f1c3a7-2d4b-4c8e-9f6a-1b3d5c8a7e2f",
  communityId: "5ea4e9c6-d3f1-4b2e-8a7d-9c1e4f6a2b8d",
  sellerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  sellerEmail: "alice@example.com",
  title: "Lisbon Photo Guide (PDF)",
  reviewedBy: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a"
}

Payload schema:

FieldTypeNullableDescription
listingIduuidListing identifier.
communityIduuidCommunity that approved.
sellerUserIduuidSeller user id.
sellerEmailstringSeller email.
titlestringListing title.
reviewedByuuidAdmin user id who approved.

Related: listing.requested, listing.rejected

listing.rejected — Listing rejected

A community admin rejected a pending marketplace listing with a reason.

When it fires. A community admin rejected a pending listing. The seller is informed; the listing does not go live.

Example payload (data field):

{
  listingId: "e5f1c3a7-2d4b-4c8e-9f6a-1b3d5c8a7e2f",
  communityId: "5ea4e9c6-d3f1-4b2e-8a7d-9c1e4f6a2b8d",
  sellerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  sellerEmail: "alice@example.com",
  title: "Lisbon Photo Guide (PDF)",
  reviewedBy: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a",
  reason: "Photos appear to be stock images rather than original work."
}

Payload schema:

FieldTypeNullableDescription
listingIduuidListing identifier.
communityIduuidCommunity that rejected.
sellerUserIduuidSeller user id.
sellerEmailstringSeller email.
titlestringListing title.
reviewedByuuidAdmin user id who rejected.
reasonstringOptional rejection reason recorded by the admin.

Related: listing.requested, listing.approved

listing.deactivated — Listing deactivated

A previously-approved marketplace listing was deactivated (admin action or auto-takedown).

When it fires. An ACTIVE marketplace listing was deactivated — admin moderation, content-policy auto-takedown, or seller account suspension. The listing is no longer purchasable but historical data is preserved.

Example payload (data field):

{
  listingId: "e5f1c3a7-2d4b-4c8e-9f6a-1b3d5c8a7e2f",
  communityId: "5ea4e9c6-d3f1-4b2e-8a7d-9c1e4f6a2b8d",
  sellerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  sellerEmail: "alice@example.com",
  title: "Lisbon Photo Guide (PDF)",
  reason: "Policy violation: misleading description."
}

Payload schema:

FieldTypeNullableDescription
listingIduuidDeactivated listing.
communityIduuidCommunity owning the listing.
sellerUserIduuidSeller user id.
sellerEmailstringSeller email.
titlestringListing title.
reasonstringOptional reason recorded by the moderator.

Related: listing.approved, listing.withdrawn

listing.withdrawn — Listing withdrawn

A seller withdrew their own marketplace listing.

When it fires. A seller pulled their own marketplace listing. The listing transitions to WITHDRAWN and is no longer purchasable.

Example payload (data field):

{
  listingId: "e5f1c3a7-2d4b-4c8e-9f6a-1b3d5c8a7e2f",
  communityId: "5ea4e9c6-d3f1-4b2e-8a7d-9c1e4f6a2b8d",
  sellerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  sellerEmail: "alice@example.com",
  title: "Lisbon Photo Guide (PDF)"
}

Payload schema:

FieldTypeNullableDescription
listingIduuidWithdrawn listing.
communityIduuidCommunity owning the listing.
sellerUserIduuidSeller user id.
sellerEmailstringSeller email.
titlestringListing title.

Related: listing.requested, listing.deactivated


Sales (orders + refunds)

sale.completed — Sale completed

A paid checkout completed — event ticket, marketplace product, or subscription first payment.

When it fires. A buyer-paid checkout completed successfully on Stripe and the sale was recorded. Fires for event tickets, marketplace products, and the first paid period of a subscription. Typed-by-domain siblings (event.ticket.purchased, product.purchased, subscription.purchase_receipt) fire alongside this generic event — subscribe to the typed event if you only care about one product class.

Example payload (data field):

{
  saleId: "6b1e3c8f-2d4a-4b7c-9e5f-8a1c3b6d9f2e",
  transactionId: "3c5a8e1b-4f7d-4c2b-9a6e-1d8f3b5c7a9e",
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  productSnapshotId: null,
  buyerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  buyerEmail: "alice@example.com",
  grossAmount: 1500,
  currency: "eur",
  productType: "EVENT"
}

Payload schema:

FieldTypeNullableDescription
saleIduuidStable sale identifier.
transactionIduuidStripe-side transaction reference stored in Cobuntu.
eventIduuidSet when the sale was for an event ticket.
productSnapshotIduuidSet when the sale was for a marketplace product (snapshot of price/title at purchase).
buyerUserIduuidBuyer user id; null for magic-link guest purchases.
buyerEmailstringBuyer email used at checkout.
grossAmountnumberGross amount in minor currency units (e.g. 1500 = €15.00).
currencystringISO 4217 currency code in lowercase (e.g. eur, usd).
productTypeEVENT | DIGITAL | SUBSCRIPTIONWhat was sold.

Related: product.purchased, subscription.purchase_receipt, refund.completed

refund.completed — Refund completed

A refund was processed on Stripe and the sale was marked refunded (FULL or PARTIAL). Seller-facing — for buyer-facing automation/emails see refund.issued.

When it fires. Stripe confirmed a refund and the sale was updated to REFUNDED (full) or PARTIALLY_REFUNDED. This is the seller-facing event — refund.issued fires the same moment with buyer-facing context for separate consumer-style handling.

Example payload (data field):

{
  refundId: "8c2a4e1b-3d5f-4a7c-9b6e-1d8f3a5c7b9e",
  saleId: "6b1e3c8f-2d4a-4b7c-9e5f-8a1c3b6d9f2e",
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  buyerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  buyerEmail: "alice@example.com",
  amount: 1500,
  refundType: "FULL",
  reason: "BUYER_REQUEST"
}

Payload schema:

FieldTypeNullableDescription
refundIduuidStable identifier for the refund.
saleIduuidThe sale being refunded — matches sale.completed.saleId.
eventIduuidSet when the underlying sale was for an event ticket.
buyerUserIduuidBuyer user id; null for guest purchases.
buyerEmailstringBuyer email.
amountnumberRefund amount in minor currency units.
refundTypeFULL | PARTIALWhether the full sale or part of it was refunded.
reasonBUYER_REQUEST | HOST_CANCELLATION | CHARGEBACKWhy the refund was issued.

Related: refund.issued, sale.completed, product.refunded

refund.issued — Refund issued (buyer-facing)

Buyer-facing refund event — same fire moment as refund.completed but exposed as its own event so leaders can wire automations that target the BUYER (e.g. drip survey, win-back) without confusing it with their internal accounting hooks on refund.completed.

When it fires. Same fire moment as refund.completed but typed for buyer-facing comms. Use this event when you want to message the buyer (survey, win-back, support follow-up) and refund.completed when you want to record the accounting side internally.

Example payload (data field):

{
  refundId: "8c2a4e1b-3d5f-4a7c-9b6e-1d8f3a5c7b9e",
  saleId: "6b1e3c8f-2d4a-4b7c-9e5f-8a1c3b6d9f2e",
  buyerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  buyerEmail: "alice@example.com",
  amount: 1500,
  refundType: "FULL",
  reason: "BUYER_REQUEST",
  productName: "Summer Rooftop Meetup — General Admission"
}

Payload schema:

FieldTypeNullableDescription
refundIduuidMatches refund.completed.refundId.
saleIduuidThe sale being refunded.
buyerUserIduuidBuyer user id; null for guest purchases.
buyerEmailstringBuyer email.
amountnumberRefund amount in minor currency units.
refundType`FULLPARTIAL`
reason`BUYER_REQUESTHOST_CANCELLATIONCHARGEBACK`
productNamestringPre-formatted product/event title for the buyer-facing email.

Related: refund.completed

event.ticket.purchased — Event ticket purchased

Typed-by-domain sibling of sale.completed for event tickets only. Lets leaders wire ticket-shaped flows (calendar invites, prep emails, attendee onboarding) without filtering generic sale events.

When it fires. A paid event-ticket checkout completed. Fires alongside sale.completed with event-specific fields. Subscribe here when you only care about event tickets (not marketplace products or subscriptions).

Example payload (data field):

{
  saleId: "6b1e3c8f-2d4a-4b7c-9e5f-8a1c3b6d9f2e",
  transactionId: "3c5a8e1b-4f7d-4c2b-9a6e-1d8f3b5c7a9e",
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  eventName: "Summer Rooftop Meetup",
  attendanceId: null,
  tierId: "a3b5c7d9-2e4f-4a6b-8c0d-1f3e5a7b9c2e",
  tierName: "General Admission",
  buyerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  buyerEmail: "alice@example.com",
  buyerName: "Alice Souza",
  grossAmount: 1500,
  currency: "eur",
  hostEmail: "host@acme.example.com",
  hostName: "Carla Rocha",
  totalTicketsSold: 38
}

Payload schema:

FieldTypeNullableDescription
saleIduuidUnderlying sale id.
transactionIduuidStripe transaction reference.
eventIduuidEvent the ticket is for.
eventNamestringEvent title at purchase time; null if the event row is missing at dispatch (rare, e.g. concurrent delete).
attendanceIduuidAttendance row id when created in the same dispatch; null when the attendance is created downstream by a separate event.attendance.registered event.
tierIduuidTicket tier id.
tierNamestringTier label.
buyerUserIduuidBuyer user id; null for guest checkouts.
buyerEmailstringBuyer email.
buyerNamestringBuyer display name.
grossAmountnumberTicket price in minor currency units.
currencystringISO 4217 currency code (lowercase).
hostEmailstringEvent host email — useful for host-facing notifications.
hostNamestringEvent host display name.
totalTicketsSoldnumberRunning count of tickets sold for this event after this purchase.

Related: sale.completed, event.attendance.registered, event.ticket.refunded

product.purchased — Product purchased

A buyer completed checkout for a marketplace product. Fires alongside sale.completed; typed-by-domain so leaders can wire product-shaped flows (post-purchase upsell, fulfillment instructions, review request).

When it fires. A paid marketplace product checkout completed. Fires alongside sale.completed with product-specific fields. Subscribe here when you only care about marketplace products (not event tickets or subscriptions).

Example payload (data field):

{
  saleId: "6b1e3c8f-2d4a-4b7c-9e5f-8a1c3b6d9f2e",
  transactionId: "3c5a8e1b-4f7d-4c2b-9a6e-1d8f3b5c7a9e",
  productId: "ad1c5e3b-7f2a-4b6c-9e1d-3f8a5c2b7d9e",
  productSnapshotId: "be2d6f4c-8a3b-4c7d-9f2e-4a9b6c3d8e1f",
  productName: "Lisbon Photo Guide (PDF)",
  buyerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  buyerEmail: "alice@example.com",
  buyerName: "Alice Souza",
  grossAmount: 2500,
  currency: "eur",
  productType: "DIGITAL"
}

Payload schema:

FieldTypeNullableDescription
saleIduuidUnderlying sale id.
transactionIduuidStripe-side transaction reference.
productIduuidProduct id (canonical); null if the product was deleted before this event was processed.
productSnapshotIduuidSnapshot at purchase time — use this for invoices since the canonical product may change.
productNamestringProduct title at purchase time.
buyerUserIduuidBuyer user id; null for guest purchases.
buyerEmailstringBuyer email.
buyerNamestringBuyer display name.
grossAmountnumberPrice in minor currency units.
currencystringISO 4217 currency code (lowercase).
productType`DIGITALEVENT`

Related: sale.completed, product.refunded

product.refunded — Product refunded

A buyer was refunded for a marketplace product — typed-by-domain split of refund.completed/refund.issued. Lets leaders subscribe specifically to product refunds (e.g. revoke digital download access, mark return shipping, follow up).

When it fires. A refund for a marketplace product completed. Fires alongside refund.completed and refund.issued. Subscribe here when you only want product refunds.

Example payload (data field):

{
  refundId: "8c2a4e1b-3d5f-4a7c-9b6e-1d8f3a5c7b9e",
  saleId: "6b1e3c8f-2d4a-4b7c-9e5f-8a1c3b6d9f2e",
  productId: "ad1c5e3b-7f2a-4b6c-9e1d-3f8a5c2b7d9e",
  productName: "Lisbon Photo Guide (PDF)",
  buyerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  buyerEmail: "alice@example.com",
  amount: 2500,
  refundType: "FULL",
  reason: "BUYER_REQUEST"
}

Payload schema:

FieldTypeNullableDescription
refundIduuidStable refund identifier.
saleIduuidOriginal sale being refunded.
productIduuidProduct id; null if deleted.
productNamestringProduct title (snapshot).
buyerUserIduuidBuyer user id; null for guest purchases.
buyerEmailstringBuyer email.
amountnumberRefund amount in minor currency units.
refundType`FULLPARTIAL`
reason`BUYER_REQUESTHOST_CANCELLATIONCHARGEBACK`

Related: product.purchased, refund.completed, refund.issued

event.ticket.refunded — Event ticket refunded

Typed-by-domain sibling of refund.issued for event tickets only. Lets leaders subscribe specifically to ticket refunds — seat-release, attendee follow-up, host notification.

When it fires. A refund for an event ticket completed. Fires alongside refund.completed + refund.issued with ticket-specific fields. Subscribe here when you only want event-ticket refunds.

Example payload (data field):

{
  refundId: "8c2a4e1b-3d5f-4a7c-9b6e-1d8f3a5c7b9e",
  saleId: "6b1e3c8f-2d4a-4b7c-9e5f-8a1c3b6d9f2e",
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  eventName: "Summer Rooftop Meetup",
  buyerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  buyerEmail: "alice@example.com",
  amount: 1500,
  refundType: "FULL",
  reason: "BUYER_REQUEST",
  productName: "Summer Rooftop Meetup — General Admission"
}

Payload schema:

FieldTypeNullableDescription
refundIduuidStable refund identifier.
saleIduuidOriginal sale being refunded.
eventIduuidEvent the refunded ticket was for.
eventNamestringEvent title at the time of refund; null if the event row is missing.
buyerUserIduuidBuyer user id; null for guest purchases.
buyerEmailstringBuyer email.
amountnumberRefund amount in minor currency units.
refundTypeFULL | PARTIALWhether the full sale or part of it was refunded.
reasonBUYER_REQUEST | HOST_CANCELLATION | CHARGEBACKWhy the refund was issued.
productNamestringPre-formatted display name (event name OR snapshot product name).

Related: event.ticket.purchased, refund.completed, refund.issued

dispute.created — Chargeback dispute opened

Stripe notified that a buyer filed a chargeback dispute. Seller has until evidenceDeadline to submit evidence or the chargeback resolves in the buyer's favor and the amount is debited from payouts.

When it fires. Stripe sent a charge.dispute.created notification — a buyer filed a chargeback. The seller has until evidenceDeadline to upload evidence in the Stripe Dashboard or the dispute auto-resolves in the buyer's favor and the amount is debited from upcoming payouts.

Example payload (data field):

{
  disputeId: "dp_1QABCDEFGHIJKLMNOP",
  saleId: "6b1e3c8f-2d4a-4b7c-9e5f-8a1c3b6d9f2e",
  chargeId: "ch_1QABCDEFGHIJKLMNOP",
  sellerCommunityId: "5ea4e9c6-d3f1-4b2e-8a7d-9c1e4f6a2b8d",
  buyerEmail: "alice@example.com",
  orderName: "Summer Rooftop Meetup — General Admission",
  disputeAmount: 1500,
  currency: "eur",
  reason: "fraudulent",
  evidenceDeadline: "2026-06-01T23:59:59Z",
  stripeDashboardUrl: "https://dashboard.stripe.com/disputes/dp_1QABCDEFGHIJKLMNOP"
}

Payload schema:

FieldTypeNullableDescription
disputeIdstringStripe dispute id (starts with dp_).
saleIduuidThe Cobuntu sale the dispute relates to.
chargeIdstringStripe charge id (starts with ch_).
sellerCommunityIduuidCommunity that received the original sale.
buyerEmailstringBuyer email used at checkout.
orderNamestringPre-formatted product/event title for display.
disputeAmountnumberDisputed amount in minor currency units.
currencystringISO 4217 currency code (lowercase).
reasonstringStripe's machine-readable reason code (e.g. fraudulent, product_not_received).
evidenceDeadlineiso-8601Deadline by which evidence must be submitted.
stripeDashboardUrlurlDeep link to manage the dispute on Stripe Dashboard.

Related: sale.completed, refund.completed

donation.received — Donation received

A buyer donated to an event or product. Fires for all three donation flows: sidecar (donation alongside a paid ticket / product), free-RSVP-with-donation (free event + tip), and standalone (donate without buying anything).

When it fires. A donation row was successfully recorded after checkout.session.completed. Idempotent on transactionId — webhook re-deliveries do not re-fire. Sidecar donations on paid sales also fire sale.completed for the underlying ticket/product; subscribe to whichever event matches your use case.

Example payload (data field):

{
  donationId: "c3a7b1e4-5f6d-4a2c-9b8e-0d1f3a5c7b9d",
  transactionId: "7d4e8c1a-9b2f-3a5c-6d7e-1f0a2b4c8d6e",
  saleId: "1a5b3c8e-2f4d-6a7c-9b0d-1e3f5c8a4b7d",
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  productId: null,
  buyerUserId: "8a4b2c6d-9e1f-3c5d-7b8a-0f2e4d6c1a3b",
  buyerEmail: null,
  amount: 500,
  currency: "EUR",
  flow: "sidecar"
}

Payload schema:

FieldTypeNullableDescription
donationIduuidStable donation identifier — the canonical idempotency key for this event.
transactionIduuidParent checkout transaction.
saleIduuidThe sale this donation attached to. For sidecar donations on paid checkouts, this points at the primary ticket/product sale. For free-RSVP-with-donation and standalone donations, this points at a synthetic sale row that the payout system uses to route the seller's share.
eventIduuidSet when the donation parent is an event. Mutually exclusive with productId.
productIduuidSet when the donation parent is a marketplace product. Mutually exclusive with eventId.
buyerUserIduuidAuthenticated donor id; null for guest donations (email-only).
buyerEmailstringGuest email when the donor checked out without an account; null when the donor was an authenticated member (use buyerUserId).
amountnumberDonation amount in minor currency units (e.g. 500 = €5.00).
currencystringISO 4217 currency code.
flowsidecar | free-rsvp-donation | donation-onlyWhich checkout flow produced this donation. Useful for routing custom thank-you content per flow.

Notes. Donation revenue routes 100% to the seller minus Stripe's percentage fee — no community commission, no platform fee.

Related: sale.completed, donation.refunded

donation.refunded — Donation refunded

A donation was refunded — either the parent sale of a sidecar donation was refunded (the donation rides with it), or a host explicitly refunded a standalone donation.

When it fires. Same fire moment as refund.completed on the underlying sale, but typed specifically for donations so consumers can subscribe to only the donation half of a multi-line refund. Fires on FULL refunds when the refunded sale carries a donation row.

v1 limitation. PARTIAL refunds don't fire this event yet — today's refund flow can't distinguish ticket vs donation portions. When the admin UI grows line-item-level refund picking, this event will fire for partial donation refunds too.

Example payload (data field):

{
  donationId: "c3a7b1e4-5f6d-4a2c-9b8e-0d1f3a5c7b9d",
  refundId: "8c2a4e1b-3d5f-4a7c-9b6e-1d8f3a5c7b9e",
  saleId: "6b1e3c8f-2d4a-4b7c-9e5f-8a1c3b6d9f2e",
  eventId: "9e2d4b7a-1c5f-4a3b-8e6c-2d9f1b4a7c3e",
  productId: null,
  buyerUserId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  buyerEmail: "alice@example.com",
  amount: 500,
  currency: "EUR",
  refundType: "FULL",
  reason: "BUYER_REQUEST"
}

Payload schema:

FieldTypeNullableDescription
donationIduuidThe donation being refunded — matches donation.received.donationId.
refundIduuidThe underlying refund record id (same as refund.completed.refundId).
saleIduuidThe sale row that carried the donation.
eventIduuidSet when the donation parent was an event.
productIduuidSet when the donation parent was a product.
buyerUserIduuidDonor user id; null for guest donations.
buyerEmailstringDonor email.
amountnumberRefunded donation amount in minor currency units.
currencystringISO 4217 currency code.
refundTypeFULLAlways FULL in v1 — partial refunds don't fire this event yet.
reasonBUYER_REQUEST | HOST_CANCELLATION | CHARGEBACK | STRIPE_EXTERNALWhy the refund was issued. STRIPE_EXTERNAL indicates a refund initiated from outside Cobuntu (e.g. directly in the Stripe Dashboard).

Notes. refund.completed fires the same moment with the parent sale's accounting context. Subscribe to this event when you only care about the donation portion (e.g. to issue a thank-you-for-trying email to a refunded donor).

Related: refund.completed, refund.issued, donation.received


Subscriptions

subscription.activated — Subscription activated

A subscription started — free segment activation OR first paid period after Stripe checkout.

When it fires. A user gained subscription access to a segment — either a free segment was activated for them, or a paid checkout completed and the first billing period started. For paid first-purchase, subscription.purchase_receipt fires alongside this for receipt-style comms.

Example payload (data field):

{
  subscriptionId: "1f3a5c7e-9b2d-4e6c-8a4d-7f2b6c9a3e5d",
  segmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  segmentName: "Founding Members",
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  billingCycle: "YEARLY",
  lockedPrice: 9900,
  currency: "eur",
  communityUrl: "https://my-community.cobuntu.com"
}

Payload schema:

FieldTypeNullableDescription
subscriptionIduuidStable subscription identifier.
segmentIduuidThe segment (tier) the user joined.
segmentNamestringHuman-readable segment name.
userIduuidSubscriber user id.
emailstringSubscriber email.
billingCycle`ONE_TIMEMONTHLYYEARLY`
lockedPricenumberPrice locked in for this subscription in minor currency units.
currencystringISO 4217 currency code (lowercase).
communityUrlurlCommunity home URL for welcome links.

Related: subscription.purchase_receipt, subscription.renewed, subscription.cancelled

subscription.renewed — Subscription renewed

A recurring subscription was successfully billed for a new period.

When it fires. Stripe successfully charged a recurring subscription for a new billing period. Fires once per renewal; failures fire subscription.payment_failed instead.

Example payload (data field):

{
  subscriptionId: "1f3a5c7e-9b2d-4e6c-8a4d-7f2b6c9a3e5d",
  segmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  currentPeriodStart: "2027-05-18T00:00:00Z",
  currentPeriodEnd: "2028-05-18T00:00:00Z",
  amount: 9900,
  tierName: "Founding Members",
  nextRenewalDate: "2028-05-18T00:00:00Z"
}

Payload schema:

FieldTypeNullableDescription
subscriptionIduuidMatches the original activation.
segmentIduuidSegment (tier) being renewed.
userIduuidSubscriber user id.
emailstringSubscriber email.
currentPeriodStartiso-8601Start of the new period that was just paid.
currentPeriodEndiso-8601End of the new period.
amountnumberAmount charged in minor currency units.
tierNamestringPre-formatted tier name for receipts.
nextRenewalDateiso-8601When the next renewal attempt will run.

Related: subscription.activated, subscription.payment_failed

subscription.payment_failed — Subscription payment failed

A recurring subscription billing failed (Stripe invoice.payment_failed). Buyer keeps access until period end.

When it fires. Stripe failed to charge a recurring subscription (card declined, expired, insufficient funds, etc.). The user keeps access until the current period ends; Stripe retries on its standard cadence. If retries succeed, subscription.renewed fires; if not, subscription.expired eventually fires.

Example payload (data field):

{
  subscriptionId: "1f3a5c7e-9b2d-4e6c-8a4d-7f2b6c9a3e5d",
  segmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  failureReason: "Your card was declined.",
  tierName: "Founding Members",
  retryDate: "2027-05-21T00:00:00Z",
  updateCardUrl: "https://billing.stripe.com/p/session/test_xyz"
}

Payload schema:

FieldTypeNullableDescription
subscriptionIduuidSubscription that failed to bill.
segmentIduuidSegment (tier) being billed.
userIduuidSubscriber user id.
emailstringSubscriber email.
failureReasonstringStripe's failure description for end-user display.
tierNamestringPre-formatted tier name.
retryDateiso-8601When Stripe will retry the charge; null when Stripe has stopped retrying.
updateCardUrlurlStripe Customer Portal link for updating payment method.

Related: subscription.renewed, subscription.expired

subscription.cancelled — Subscription cancelled

A subscription was cancelled by the user (e.g. via billing portal). Buyer keeps access until period end; expiry fires subscription.expired separately. For admin-initiated cancellations see subscription.cancelled_by_admin.

When it fires. The subscriber cancelled their own subscription — typically via the Stripe Customer Portal or in-app cancel button. The user keeps access until the current period ends; subscription.expired fires at that point.

Example payload (data field):

{
  subscriptionId: "1f3a5c7e-9b2d-4e6c-8a4d-7f2b6c9a3e5d",
  segmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  cancelledBy: "user",
  currentPeriodEnd: "2028-05-18T00:00:00Z",
  tierName: "Founding Members",
  accessUntilDate: "2028-05-18T00:00:00Z"
}

Payload schema:

FieldTypeNullableDescription
subscriptionIduuidSubscription cancelled.
segmentIduuidSegment (tier) being cancelled.
userIduuidSubscriber user id.
emailstringSubscriber email.
cancelledByuserAlways user for this event; admin cancellations fire subscription.cancelled_by_admin.
currentPeriodEndiso-8601Effective access end — same as accessUntilDate.
tierNamestringPre-formatted tier name.
accessUntilDateiso-8601When the user loses access (period end).

Related: subscription.cancelled_by_admin, subscription.expired

subscription.cancelled_by_admin — Subscription cancelled by admin

An admin cancelled a member's subscription (vs the user cancelling themselves). Distinct from subscription.cancelled so leaders can apologize / explain / offer reactivation in their cancellation flow.

When it fires. An admin (not the subscriber) cancelled a member's subscription from the admin app. Distinct from subscription.cancelled so admin cancellation flows can apologize, explain, or offer reactivation without the wrong tone.

Example payload (data field):

{
  subscriptionId: "1f3a5c7e-9b2d-4e6c-8a4d-7f2b6c9a3e5d",
  segmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  segmentName: "Founding Members",
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  cancelledBy: "admin",
  adminUserId: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a",
  reason: "Member requested cancellation via support.",
  currentPeriodEnd: "2028-05-18T00:00:00Z"
}

Payload schema:

FieldTypeNullableDescription
subscriptionIduuidSubscription cancelled.
segmentIduuidSegment (tier) being cancelled.
segmentNamestringHuman-readable segment name.
userIduuidSubscriber user id.
emailstringSubscriber email.
cancelledByadminAlways admin for this event.
adminUserIduuidThe admin who performed the cancellation.
reasonstringOptional reason recorded by the admin.
currentPeriodEndiso-8601Period end if access continues until then; null if immediate.

Related: subscription.cancelled, subscription.expired

subscription.expired — Subscription expired

A subscription's access period ended — natural end-of-period OR forced expiry from a FULL refund.

When it fires. A subscription's access period ended — either the user cancelled and the period naturally elapsed, or a FULL refund was issued and access was revoked. The user no longer has tier access from this point.

Example payload (data field):

{
  subscriptionId: "1f3a5c7e-9b2d-4e6c-8a4d-7f2b6c9a3e5d",
  segmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  expiredAt: "2028-05-18T00:00:00Z",
  reason: "period_end",
  tierName: "Founding Members",
  resubscribeUrl: "https://my-community.cobuntu.com/membership/founding"
}

Payload schema:

FieldTypeNullableDescription
subscriptionIduuidExpired subscription.
segmentIduuidSegment (tier) access was lost from.
userIduuidFormer subscriber user id.
emailstringFormer subscriber email.
expiredAtiso-8601When access ended.
reason`period_endrefund`
tierNamestringPre-formatted tier name.
resubscribeUrlurlDeep link to re-subscribe; null when the tier is no longer for sale.

Related: subscription.cancelled, subscription.cancelled_by_admin

subscription.purchase_receipt — Subscription purchase receipt

First successful payment on a paid subscription. Distinct from subscription.activated (welcome) so leaders can ship a clean receipt (you paid €X for tier Y, invoice attached) without conflating with onboarding comms. Fires once per subscription on the FIRST charge — recurring renewals fire subscription.renewed instead.

When it fires. Fires once per paid subscription, on the very first successful charge. Use this for receipt-style comms; use subscription.activated for welcome/onboarding (which fires at the same dispatch tick).

Example payload (data field):

{
  subscriptionId: "1f3a5c7e-9b2d-4e6c-8a4d-7f2b6c9a3e5d",
  segmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  segmentName: "Founding Members",
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  amount: 9900,
  currency: "eur",
  billingCycle: "YEARLY",
  invoiceUrl: "https://invoice.stripe.com/i/abc123",
  paidAt: "2026-05-18T09:00:00Z"
}

Payload schema:

FieldTypeNullableDescription
subscriptionIduuidSubscription being receipted.
segmentIduuidSegment (tier) joined.
segmentNamestringHuman-readable segment name.
userIduuidSubscriber user id.
emailstringSubscriber email.
amountnumberFirst-charge amount in minor currency units.
currencystringISO 4217 currency code (lowercase).
billingCycle`MONTHLYYEARLYONE_TIME`
invoiceUrlurlStripe-hosted invoice URL for download.
paidAtiso-8601When the charge cleared.

Related: subscription.activated, subscription.renewed

tier.changed — Tier changed

A member moved between tiers — upgrade or downgrade. Distinct from a fresh subscription.activated so leaders can wire dedicated tier-change comms (price-diff explanation, prorate notice).

When it fires. An existing subscriber moved between tiers — upgrade, downgrade, or sideways. Distinct from subscription.activated so tier-change comms (proration explanation, congrats / sorry-to-see-you-drop messaging) stay clean.

Example payload (data field):

{
  subscriptionId: "1f3a5c7e-9b2d-4e6c-8a4d-7f2b6c9a3e5d",
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  fromSegmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  fromSegmentName: "Supporter",
  toSegmentId: "c6f2e8d1-3b5a-4d7e-9f1c-8a4b2d6f3e9c",
  toSegmentName: "Founding Members",
  direction: "upgrade",
  lockedPrice: 9900,
  currency: "eur"
}

Payload schema:

FieldTypeNullableDescription
subscriptionIduuidSubscription whose tier changed.
userIduuidSubscriber user id.
emailstringSubscriber email.
fromSegmentIduuidPrevious segment (tier).
fromSegmentNamestringPrevious tier name.
toSegmentIduuidNew segment (tier).
toSegmentNamestringNew tier name.
direction`upgradedowngradelateral`
lockedPricenumberLocked-in price on the new tier (minor currency units).
currencystringISO 4217 currency code (lowercase).

Related: subscription.activated

member.discount.ended — Member discount ended

Member-tier-scoped pricing on a product subscription lapsed because the buyer's segment membership ended. The base price now applies.

When it fires. A buyer was paying a member-tier-discounted price on a product subscription. Their segment membership ended (cancellation, expiry, kick) — the discount is no longer valid, so the next renewal will be at the base price. Fires once per affected product subscription at lapse time. Use this to warn the buyer their effective price is about to go up, or to offer a re-enrol path.

Example payload (data field):

{
  productSubscriptionId: "a7b3c5d9-1e2f-4a6c-8b0d-3f5e7a9c1b2d",
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  productId: "9c1d5b8e-4a2f-6e3d-8b7c-1f0a2d4e6c9b",
  productName: "Weekly mentor sessions",
  endedSegmentId: "7b3a9c1e-2d4f-4a8b-9e0c-1f5a3b7d9c2e",
  endedSegmentName: "Founding Members",
  previousPrice: 2000,
  basePrice: 3500,
  currency: "eur",
  effectiveFrom: "2026-06-15T00:00:00Z"
}

Payload schema:

FieldTypeNullableDescription
productSubscriptionIduuidAffected product subscription identifier.
userIduuidSubscriber user id.
emailstringSubscriber email.
productIduuidProduct the subscription is for.
productNamestringProduct name (snapshot).
endedSegmentIduuidSegment membership that lapsed and removed the discount.
endedSegmentNamestringLapsed segment name.
previousPricenumberDiscounted price the buyer was paying (minor currency units).
basePricenumberBase product price that will apply at next renewal.
currencystringISO 4217 currency code (lowercase).
effectiveFromiso-8601When the new base price takes effect (typically next renewal date).

Related: subscription.renewed, tier.changed


Payouts & finance

payout.confirmed — Payout confirmed

A scheduled payout settled successfully to the connected account's bank.

When it fires. Stripe confirmed a scheduled payout settled to the connected community's bank account. The funds are out of Stripe and on their way (or already arrived) at the bank.

Example payload (data field):

{
  payoutId: "4d6c8a2e-3f5b-4e7c-9a1d-8b3f5c9a7e2d",
  communityId: "5ea4e9c6-d3f1-4b2e-8a7d-9c1e4f6a2b8d",
  recipientUserId: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a",
  recipientEmail: "founder@example.com",
  amount: 60240,
  currency: "eur",
  stripeAccountId: "acct_1OABCDEFGHIJKLM",
  arrivalDate: "2026-05-22T00:00:00Z"
}

Payload schema:

FieldTypeNullableDescription
payoutIduuidCobuntu payout identifier.
communityIduuidCommunity whose Stripe account received the payout.
recipientUserIduuidUser id of the Stripe account owner; null for community-owned accounts.
recipientEmailstringEmail used on the connected Stripe account.
amountnumberPayout amount in minor currency units.
currencystringISO 4217 currency code (lowercase).
stripeAccountIdstringStripe Connect account id (starts with acct_).
arrivalDateiso-8601Expected arrival date at the bank — Stripe's estimate.

Related: payout.failed, commission.held

payout.failed — Payout failed

A scheduled payout failed to settle (insufficient balance, account restricted, bank rejection).

When it fires. A scheduled payout did not settle — insufficient balance, account restricted, bank rejection, or wire failure. Surface to the recipient with the reason and prompt for a fix.

Example payload (data field):

{
  payoutId: "4d6c8a2e-3f5b-4e7c-9a1d-8b3f5c9a7e2d",
  communityId: "5ea4e9c6-d3f1-4b2e-8a7d-9c1e4f6a2b8d",
  recipientUserId: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a",
  recipientEmail: "founder@example.com",
  amount: 60240,
  currency: "eur",
  stripeAccountId: "acct_1OABCDEFGHIJKLM",
  failureReason: "Bank account closed.",
  failureCode: "account_closed"
}

Payload schema:

FieldTypeNullableDescription
payoutIduuidPayout identifier.
communityIduuidCommunity whose payout failed.
recipientUserIduuidAccount owner user id; null for community-owned accounts.
recipientEmailstringAccount owner email.
amountnumberAttempted payout amount in minor currency units.
currencystringISO 4217 currency code (lowercase).
stripeAccountIdstringStripe Connect account id.
failureReasonstringHuman-readable failure description from Stripe.
failureCodestringStripe machine-readable failure code (e.g. account_closed); null when Stripe didn't provide one.

Related: payout.confirmed

commission.held — Commission held

A commission was held back from a payout (escrow, dispute reserve, refund offset).

When it fires. Cobuntu held back commission from a community's payout — typical reasons are escrow until event delivery, dispute reserve held during a chargeback investigation, or refund offset against prior sales.

Example payload (data field):

{
  commissionId: "9a1c3e5b-2d4f-4a7c-8e6b-1f5a3c7d9b2e",
  communityId: "5ea4e9c6-d3f1-4b2e-8a7d-9c1e4f6a2b8d",
  recipientUserId: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a",
  recipientEmail: "founder@example.com",
  amount: 5000,
  currency: "eur",
  holdReason: "ESCROW",
  expectedReleaseDate: "2026-05-25T00:00:00Z"
}

Payload schema:

FieldTypeNullableDescription
commissionIduuidCommission identifier when backed by a commissions_held row; null when the event represents an intended-but-skipped payout (e.g. community has no Stripe Connect account configured) with no DB-backed hold entity yet.
communityIduuidCommunity whose commission is held.
recipientUserIduuidRecipient user id; null for community-owned accounts.
recipientEmailstringRecipient email.
amountnumberHeld amount in minor currency units.
currencystringISO 4217 currency code (lowercase).
holdReasonESCROW | DISPUTE_RESERVE | REFUND_OFFSET | NO_STRIPE_ACCOUNT | OTHERWhy the commission is held. NO_STRIPE_ACCOUNT fires when payout was skipped because the community hasn't connected Stripe.
expectedReleaseDateiso-8601When the hold is expected to release; null when indeterminate (e.g. requires admin action to set up Stripe).

Related: payout.confirmed, dispute.created

stripe.account.requires_verification — Stripe verification required

Stripe Connect account needs additional verification or documents — leader risks losing payouts if not addressed by the deadline.

When it fires. Stripe flagged the connected account as needing additional verification — documents, ownership, or business info. If not addressed by the deadline, Stripe may pause charges or payouts. Time-sensitive — surface in-product within minutes.

Example payload (data field):

{
  stripeAccountId: "acct_1OABCDEFGHIJKLM",
  communityId: "5ea4e9c6-d3f1-4b2e-8a7d-9c1e4f6a2b8d",
  ownerEmail: "founder@acme.example.com",
  requirements: ["individual.id_number", "individual.verification.document"],
  currentDeadline: "2026-06-01T23:59:59Z",
  verificationUrl: "https://connect.stripe.com/express/onboarding/acct_1OABCDEFGHIJKLM"
}

Payload schema:

FieldTypeNullableDescription
stripeAccountIdstringStripe Connect account id (starts with acct_).
communityIduuidCommunity owning the connected account.
ownerEmailstringAccount owner email.
requirementsstring[]Stripe requirement keys (e.g. individual.id_number) needing action.
currentDeadlineiso-8601Deadline before Stripe restricts the account; null when no deadline yet.
verificationUrlurlDeep link to Stripe's hosted onboarding to complete verification.

Related: stripe.account.payouts_disabled

stripe.account.payouts_disabled — Stripe payouts disabled

Stripe disabled payouts on the connected account. Sales still complete; funds accumulate until the account is restored.

When it fires. Stripe disabled payouts on the connected account — typically a follow-on of stripe.account.requires_verification going past deadline, or a Stripe risk-team action. Sales can still complete; funds accumulate in the Stripe balance until the account is restored.

Example payload (data field):

{
  stripeAccountId: "acct_1OABCDEFGHIJKLM",
  communityId: "5ea4e9c6-d3f1-4b2e-8a7d-9c1e4f6a2b8d",
  ownerEmail: "founder@acme.example.com",
  reason: "verification_overdue",
  currentlyDueRequirements: ["individual.verification.document"]
}

Payload schema:

FieldTypeNullableDescription
stripeAccountIdstringStripe Connect account id.
communityIduuidCommunity owning the account.
ownerEmailstringAccount owner email.
reasonstringStripe-provided reason payouts were disabled.
currentlyDueRequirementsstring[]Outstanding requirement keys blocking payouts.

Related: stripe.account.requires_verification, payout.failed


Communications

broadcast.sent — Broadcast sent

A mass email broadcast (newsletter campaign) finished sending to its recipient list. Useful for tracking send velocity in third-party analytics + triggering follow-up automations.

When it fires. A newsletter broadcast finished dispatching to its full recipient list. Fires once per campaign at end-of-send, regardless of per-recipient delivery outcomes.

Example payload (data field):

{
  campaignId: "5d9c3a7e-1b4f-4d2c-8a6b-2f9e5c7d3a1b",
  subject: "Weekly digest — 18 May",
  audience: "segment",
  recipientCount: 1247,
  senderUserId: "8d3c5a9e-1f4b-4c2d-9a7e-3b5d8f1c6e2a",
  sentAt: "2026-05-18T09:00:00Z",
  source: "admin_ui"
}

Payload schema:

FieldTypeNullableDescription
campaignIduuidCampaign identifier.
subjectstringEmail subject line sent.
audience`allsegmentjson-config`
recipientCountnumberNumber of recipients the broadcast was sent to.
senderUserIduuidAdmin user who initiated the send; null for API-triggered sends.
sentAtiso-8601When the broadcast finished sending.
source`admin_uipublic_api`

digest.scheduled — Engagement digest scheduled

Fires when the scheduled engagement-digest cron triggers a send. Per-recipient, so flows can filter on member preferences.

When it fires. The engagement-digest cron fires per-recipient on the configured cadence (daily or weekly). Each delivery dispatches its own event so leaders can filter on member preferences or skip dates.

Example payload (data field):

{
  userId: "2c8f4b1d-9a5e-4c3b-8d7f-1e6a9b2c5d4f",
  email: "alice@example.com",
  cadence: "WEEKLY",
  windowStart: "2026-05-11T00:00:00Z",
  windowEnd: "2026-05-18T00:00:00Z"
}

Payload schema:

FieldTypeNullableDescription
userIduuidRecipient user id.
emailstringRecipient email.
cadence`DAILYWEEKLY`
windowStartiso-8601Start of the activity window the digest summarizes.
windowEndiso-8601End of the activity window.

Source of truth

This page is the canonical reference for every event Cobuntu emits. Any drift between the documented payload and what your receiver sees on the wire is a bug — file an issue and we'll fix it.

The same data is also surfaced inside cobuntu-admin → Integrations → Documentation (per-community, searchable). The public OpenAPI spec at api.cobuntu.com/openapi.json ships in the next docs release.