Event catalog
Cobuntu emits 36 webhook events across 8 domains. Subscribe to whichever ones you need in admin → Integrations → Webhooks.
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
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:
| Field | Type | Nullable | Description |
|---|---|---|---|
requestId | uuid | Stable identifier for this membership request. | |
segmentId | uuid | ✓ | Segment the request targets; null when applying without a specific tier. |
segmentName | string | ✓ | Human-readable segment name, mirrors segmentId. |
email | string | Email submitted on the form. | |
name | string | ✓ | Display name; null when the form didn't collect one. |
source | string | Where the request came from — e.g. tally, jotform, landing_page, csv_import, admin_added. | |
answers | object | Raw 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://orbis.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:
| Field | Type | Nullable | Description |
|---|---|---|---|
email | string | Invitee's email address. | |
inviterName | string | Display name of the admin who sent the invitation. | |
inviterAvatar | string | ✓ | Profile-image URL of the inviter; null when no avatar set. |
customMessage | string | ✓ | Optional personal message the admin attached to the invitation. |
acceptUrl | string | Tokenised URL the invitee opens to accept. Points at the community subdomain's /join (or admin.cobuntu.com/join for role-gated invites). | |
segmentName | string | ✓ | Human-readable name of the tier the invite targets; null when no tier was specified. |
segmentId | uuid | ✓ | Tier (segment) the invite targets; null when none was specified. |
communityDescription | string | ✓ | Short 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
requestId | uuid | Matches the requestId from the original member.requested event. | |
email | string | Approved member's email. | |
name | string | ✓ | Display 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
requestId | uuid | Matches the requestId from the original member.requested event. | |
email | string | Rejected applicant's email. | |
name | string | ✓ | Display 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
segmentId | uuid | ✓ | Segment the form was scoped to; null when unscoped. |
segmentName | string | ✓ | Human-readable segment name. |
email | string | Submitted email. | |
name | string | ✓ | Display name. |
source | string | Form provider — tally, jotform, landing_page. | |
answers | object | Raw 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
userId | uuid | The kicked member's user id. | |
usertag | string | Their @usertag (handle). | |
email | string | Email at the time of kick. | |
name | string | ✓ | Display name; null when unknown. |
kickedBy | uuid | The 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
userId | uuid | The banned member's user id. | |
usertag | string | Their @usertag (handle). | |
email | string | Email at the time of ban. | |
name | string | ✓ | Display name; null when unknown. |
bannedBy | uuid | The admin user id who issued the ban. | |
reason | string | ✓ | Free-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:
| Field | Type | Nullable | Description |
|---|---|---|---|
requestId | uuid | Matches the original member.requested event. | |
userId | uuid | ✓ | User id if the applicant had an account; null for anonymous form submissions. |
email | string | Applicant email. | |
name | string | ✓ | Display name; null when unknown. |
segmentId | uuid | ✓ | Segment 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
id | uuid | Stable article identifier. | |
title | string | Article title at the time of publish. | |
slug | string | URL-safe slug used in the article's public URL. | |
excerpt | string | Short summary intended for cards and previews. | |
bannerUrl | url | ✓ | Hero image; null when no banner was uploaded. |
category | string | ✓ | Editorial category; null when uncategorized. |
publishedAt | iso-8601 | When 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
id | uuid | The article's stable identifier — useful for cleaning up external mirrors. | |
title | string | Article title at the time of deletion (snapshot for logging). | |
slug | string | Slug 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
eventId | uuid | Stable event identifier. | |
slug | string | URL-safe slug used in the event's public URL. | |
name | string | Event title. | |
startDate | iso-8601 | ✓ | Event start; null for date-less events. |
endDate | iso-8601 | ✓ | Event end; null when not set or when same-day with implicit length. |
accessibility | `PUBLIC | MEMBERS_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:
| Field | Type | Nullable | Description |
|---|---|---|---|
eventId | uuid | Matches the eventId from event.published. | |
slug | string | Slug that was in use. | |
name | string | Event title. | |
action | `unpublished | deleted` | |
attendeeCount | number | Number of registered attendees at cancellation time — useful for sizing follow-up comms. |
Related: event.published, refund.completed
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:
| Field | Type | Nullable | Description |
|---|---|---|---|
listingId | uuid | Stable listing identifier. | |
communityId | uuid | Community the listing was submitted to. | |
sellerUserId | uuid | Seller user id. | |
sellerEmail | string | Seller email. | |
title | string | Submitted listing title. | |
listingType | `PRODUCT | SERVICE` | |
price | number | Listed price in minor currency units. | |
currency | string | ISO 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
listingId | uuid | Listing identifier. | |
communityId | uuid | Community that approved. | |
sellerUserId | uuid | Seller user id. | |
sellerEmail | string | Seller email. | |
title | string | Listing title. | |
reviewedBy | uuid | Admin 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
listingId | uuid | Listing identifier. | |
communityId | uuid | Community that rejected. | |
sellerUserId | uuid | Seller user id. | |
sellerEmail | string | Seller email. | |
title | string | Listing title. | |
reviewedBy | uuid | Admin user id who rejected. | |
reason | string | ✓ | Optional 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
listingId | uuid | Deactivated listing. | |
communityId | uuid | Community owning the listing. | |
sellerUserId | uuid | Seller user id. | |
sellerEmail | string | Seller email. | |
title | string | Listing title. | |
reason | string | ✓ | Optional 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
listingId | uuid | Withdrawn listing. | |
communityId | uuid | Community owning the listing. | |
sellerUserId | uuid | Seller user id. | |
sellerEmail | string | Seller email. | |
title | string | Listing 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
saleId | uuid | Stable sale identifier. | |
transactionId | uuid | Stripe-side transaction reference stored in Cobuntu. | |
eventId | uuid | ✓ | Set when the sale was for an event ticket. |
productSnapshotId | uuid | ✓ | Set when the sale was for a marketplace product (snapshot of price/title at purchase). |
buyerUserId | uuid | ✓ | Buyer user id; null for magic-link guest purchases. |
buyerEmail | string | Buyer email used at checkout. | |
grossAmount | number | Gross amount in minor currency units (e.g. 1500 = €15.00). | |
currency | string | ISO 4217 currency code in lowercase (e.g. eur, usd). | |
productType | `EVENT | DIGITAL | SUBSCRIPTION` |
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. The [System] Refund processed — seller flow installed by M4 listens on this event.
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:
| Field | Type | Nullable | Description |
|---|---|---|---|
refundId | uuid | Stable identifier for the refund. | |
saleId | uuid | The sale being refunded — matches sale.completed.saleId. | |
eventId | uuid | ✓ | Set when the underlying sale was for an event ticket. |
buyerUserId | uuid | ✓ | Buyer user id; null for guest purchases. |
buyerEmail | string | Buyer email. | |
amount | number | Refund amount in minor currency units. | |
refundType | `FULL | PARTIAL` | |
reason | `BUYER_REQUEST | HOST_CANCELLATION | CHARGEBACK` |
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:
| Field | Type | Nullable | Description |
|---|---|---|---|
refundId | uuid | Matches refund.completed.refundId. | |
saleId | uuid | The sale being refunded. | |
buyerUserId | uuid | ✓ | Buyer user id; null for guest purchases. |
buyerEmail | string | Buyer email. | |
amount | number | Refund amount in minor currency units. | |
refundType | `FULL | PARTIAL` | |
reason | `BUYER_REQUEST | HOST_CANCELLATION | CHARGEBACK` |
productName | string | ✓ | Pre-formatted product/event title for the buyer-facing email. |
Related: refund.completed
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:
| Field | Type | Nullable | Description |
|---|---|---|---|
saleId | uuid | Underlying sale id. | |
transactionId | uuid | Stripe-side transaction reference. | |
productId | uuid | ✓ | Product id (canonical); null if the product was deleted before this event was processed. |
productSnapshotId | uuid | ✓ | Snapshot at purchase time — use this for invoices since the canonical product may change. |
productName | string | Product title at purchase time. | |
buyerUserId | uuid | ✓ | Buyer user id; null for guest purchases. |
buyerEmail | string | Buyer email. | |
buyerName | string | ✓ | Buyer display name. |
grossAmount | number | Price in minor currency units. | |
currency | string | ISO 4217 currency code (lowercase). | |
productType | `DIGITAL | EVENT` |
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:
| Field | Type | Nullable | Description |
|---|---|---|---|
refundId | uuid | Stable refund identifier. | |
saleId | uuid | Original sale being refunded. | |
productId | uuid | ✓ | Product id; null if deleted. |
productName | string | Product title (snapshot). | |
buyerUserId | uuid | ✓ | Buyer user id; null for guest purchases. |
buyerEmail | string | Buyer email. | |
amount | number | Refund amount in minor currency units. | |
refundType | `FULL | PARTIAL` | |
reason | `BUYER_REQUEST | HOST_CANCELLATION | CHARGEBACK` |
Related: product.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:
| Field | Type | Nullable | Description |
|---|---|---|---|
disputeId | string | Stripe dispute id (starts with dp_). | |
saleId | uuid | The Cobuntu sale the dispute relates to. | |
chargeId | string | Stripe charge id (starts with ch_). | |
sellerCommunityId | uuid | Community that received the original sale. | |
buyerEmail | string | Buyer email used at checkout. | |
orderName | string | Pre-formatted product/event title for display. | |
disputeAmount | number | Disputed amount in minor currency units. | |
currency | string | ISO 4217 currency code (lowercase). | |
reason | string | Stripe's machine-readable reason code (e.g. fraudulent, product_not_received). | |
evidenceDeadline | iso-8601 | Deadline by which evidence must be submitted. | |
stripeDashboardUrl | url | Deep link to manage the dispute on Stripe Dashboard. |
Related: sale.completed, refund.completed
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://orbis.cobuntu.com"
}Payload schema:
| Field | Type | Nullable | Description |
|---|---|---|---|
subscriptionId | uuid | Stable subscription identifier. | |
segmentId | uuid | The segment (tier) the user joined. | |
segmentName | string | Human-readable segment name. | |
userId | uuid | Subscriber user id. | |
email | string | Subscriber email. | |
billingCycle | `ONE_TIME | MONTHLY | YEARLY` |
lockedPrice | number | Price locked in for this subscription in minor currency units. | |
currency | string | ISO 4217 currency code (lowercase). | |
communityUrl | url | Community 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
subscriptionId | uuid | Matches the original activation. | |
segmentId | uuid | Segment (tier) being renewed. | |
userId | uuid | Subscriber user id. | |
email | string | Subscriber email. | |
currentPeriodStart | iso-8601 | Start of the new period that was just paid. | |
currentPeriodEnd | iso-8601 | End of the new period. | |
amount | number | Amount charged in minor currency units. | |
tierName | string | Pre-formatted tier name for receipts. | |
nextRenewalDate | iso-8601 | When 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
subscriptionId | uuid | Subscription that failed to bill. | |
segmentId | uuid | Segment (tier) being billed. | |
userId | uuid | Subscriber user id. | |
email | string | Subscriber email. | |
failureReason | string | ✓ | Stripe's failure description for end-user display. |
tierName | string | Pre-formatted tier name. | |
retryDate | iso-8601 | ✓ | When Stripe will retry the charge; null when Stripe has stopped retrying. |
updateCardUrl | url | Stripe 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
subscriptionId | uuid | Subscription cancelled. | |
segmentId | uuid | Segment (tier) being cancelled. | |
userId | uuid | Subscriber user id. | |
email | string | Subscriber email. | |
cancelledBy | user | Always user for this event; admin cancellations fire subscription.cancelled_by_admin. | |
currentPeriodEnd | iso-8601 | Effective access end — same as accessUntilDate. | |
tierName | string | Pre-formatted tier name. | |
accessUntilDate | iso-8601 | When 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
subscriptionId | uuid | Subscription cancelled. | |
segmentId | uuid | Segment (tier) being cancelled. | |
segmentName | string | Human-readable segment name. | |
userId | uuid | Subscriber user id. | |
email | string | Subscriber email. | |
cancelledBy | admin | Always admin for this event. | |
adminUserId | uuid | The admin who performed the cancellation. | |
reason | string | ✓ | Optional reason recorded by the admin. |
currentPeriodEnd | iso-8601 | ✓ | Period 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://orbis.cobuntu.com/membership/founding"
}Payload schema:
| Field | Type | Nullable | Description |
|---|---|---|---|
subscriptionId | uuid | Expired subscription. | |
segmentId | uuid | Segment (tier) access was lost from. | |
userId | uuid | Former subscriber user id. | |
email | string | Former subscriber email. | |
expiredAt | iso-8601 | When access ended. | |
reason | `period_end | refund` | |
tierName | string | Pre-formatted tier name. | |
resubscribeUrl | url | ✓ | Deep 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
subscriptionId | uuid | Subscription being receipted. | |
segmentId | uuid | Segment (tier) joined. | |
segmentName | string | Human-readable segment name. | |
userId | uuid | Subscriber user id. | |
email | string | Subscriber email. | |
amount | number | First-charge amount in minor currency units. | |
currency | string | ISO 4217 currency code (lowercase). | |
billingCycle | `MONTHLY | YEARLY | ONE_TIME` |
invoiceUrl | url | ✓ | Stripe-hosted invoice URL for download. |
paidAt | iso-8601 | When 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:
| Field | Type | Nullable | Description |
|---|---|---|---|
subscriptionId | uuid | Subscription whose tier changed. | |
userId | uuid | Subscriber user id. | |
email | string | Subscriber email. | |
fromSegmentId | uuid | Previous segment (tier). | |
fromSegmentName | string | Previous tier name. | |
toSegmentId | uuid | New segment (tier). | |
toSegmentName | string | New tier name. | |
direction | `upgrade | downgrade | lateral` |
lockedPrice | number | Locked-in price on the new tier (minor currency units). | |
currency | string | ISO 4217 currency code (lowercase). |
Related: subscription.activated
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@orbis.example.com",
amount: 60240,
currency: "eur",
stripeAccountId: "acct_1OABCDEFGHIJKLM",
arrivalDate: "2026-05-22T00:00:00Z"
}Payload schema:
| Field | Type | Nullable | Description |
|---|---|---|---|
payoutId | uuid | Cobuntu payout identifier. | |
communityId | uuid | Community whose Stripe account received the payout. | |
recipientUserId | uuid | ✓ | User id of the Stripe account owner; null for community-owned accounts. |
recipientEmail | string | Email used on the connected Stripe account. | |
amount | number | Payout amount in minor currency units. | |
currency | string | ISO 4217 currency code (lowercase). | |
stripeAccountId | string | Stripe Connect account id (starts with acct_). | |
arrivalDate | iso-8601 | Expected 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@orbis.example.com",
amount: 60240,
currency: "eur",
stripeAccountId: "acct_1OABCDEFGHIJKLM",
failureReason: "Bank account closed.",
failureCode: "account_closed"
}Payload schema:
| Field | Type | Nullable | Description |
|---|---|---|---|
payoutId | uuid | Payout identifier. | |
communityId | uuid | Community whose payout failed. | |
recipientUserId | uuid | ✓ | Account owner user id; null for community-owned accounts. |
recipientEmail | string | Account owner email. | |
amount | number | Attempted payout amount in minor currency units. | |
currency | string | ISO 4217 currency code (lowercase). | |
stripeAccountId | string | Stripe Connect account id. | |
failureReason | string | Human-readable failure description from Stripe. | |
failureCode | string | ✓ | Stripe 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@orbis.example.com",
amount: 5000,
currency: "eur",
holdReason: "ESCROW",
expectedReleaseDate: "2026-05-25T00:00:00Z"
}Payload schema:
| Field | Type | Nullable | Description |
|---|---|---|---|
commissionId | uuid | Commission identifier. | |
communityId | uuid | Community whose commission is held. | |
recipientUserId | uuid | ✓ | Recipient user id; null for community-owned accounts. |
recipientEmail | string | Recipient email. | |
amount | number | Held amount in minor currency units. | |
currency | string | ISO 4217 currency code (lowercase). | |
holdReason | `ESCROW | DISPUTE_RESERVE | REFUND_OFFSET |
expectedReleaseDate | iso-8601 | ✓ | When the hold is expected to release; null when indeterminate. |
Related: payout.confirmed, dispute.created
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:
| Field | Type | Nullable | Description |
|---|---|---|---|
campaignId | uuid | Campaign identifier. | |
subject | string | Email subject line sent. | |
audience | `all | segment | json-config` |
recipientCount | number | Number of recipients the broadcast was sent to. | |
senderUserId | uuid | ✓ | Admin user who initiated the send; null for API-triggered sends. |
sentAt | iso-8601 | When the broadcast finished sending. | |
source | `admin_ui | public_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. Replaces the legacy direct-fire path in DigestService once M10 migrates the cron.
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:
| Field | Type | Nullable | Description |
|---|---|---|---|
userId | uuid | Recipient user id. | |
email | string | Recipient email. | |
cadence | `DAILY | WEEKLY` | |
windowStart | iso-8601 | Start of the activity window the digest summarizes. | |
windowEnd | iso-8601 | End of the activity window. |
Source of truth
This page is generated from services/core/src/shared/lifecycle/lifecycleCatalog.ts (opens in a new tab) in the backend monorepo. Any drift is a bug — file an issue if you spot one.
The same data also renders inside cobuntu-admin → Integrations → Documentation (per-community, searchable). The public OpenAPI spec at api.cobuntu.com/openapi.json ships in the next docs release.