User Journey¶
This guide documents the primary user flows through the Revel platform, covering both attendee and organizer perspectives.
This guide is a curated overview
This page covers the primary flows at a glance. It is not exhaustive — for the full, canonical, persona-based user journey map (used to drive E2E test cases), see USER_JOURNEYS.md in the repository root. Where a subsystem has a dedicated architecture page, the relevant section links to it.
Account Flows¶
Registration & Email Verification¶
New users register via the API, receive a verification email, and confirm their address.
sequenceDiagram
participant U as User
participant API as POST /api/account/register
participant S as register_user()
participant C as Celery
participant E as Email
participant V as POST /api/account/verify
U->>API: Submit registration
API->>S: Validate & create user
S->>C: Dispatch verification task
C->>E: Send verification email
E-->>U: Email with verification link
U->>V: Click verification link
V-->>U: Account verified Authentication¶
POST /api/auth/token/pair
Content-Type: application/json
{
"username": "[email protected]",
"password": "secret"
}
Returns an access/refresh JWT token pair. The field is username (it holds the email address: every account's username is its email).
If the user has TOTP enabled, the initial login returns a partial token. The user must then submit the TOTP code to complete authentication.
Users can authenticate through any configured OpenID Connect provider (Google on the hosted instance; Keycloak, Authentik, or an institutional IdP on self-hosted ones). The browser is sent to GET /api/auth/oidc/{provider}/start, comes back on the provider's callback, and the frontend exchanges the resulting one-time token for Revel JWT tokens via POST /api/auth/oidc/exchange.
Configuration
Providers are listed in OIDC_PROVIDERS; GET /version exposes them as sso_providers so the frontend only shows buttons for what is configured. With no providers configured the routes return 404.
Profile & Password Management¶
| Endpoint | Method | Description |
|---|---|---|
/api/account/me | GET | Retrieve current user profile |
/api/account/me | PUT | Update profile fields |
/api/account/password/reset-request | POST | Request password reset email |
/api/account/password/reset | POST | Confirm reset with token + new password |
Account Deletion¶
sequenceDiagram
participant U as User
participant API as API
participant C as Celery
participant E as Email
U->>API: POST /api/account/delete-request
API->>C: Dispatch confirmation email
C->>E: Send deletion confirmation
E-->>U: Email with confirmation link
U->>API: POST /api/account/delete-confirm
API-->>U: Account deleted Payment records
Payment records cascade-delete with the user account. This works because Stripe retains the financial records on their side for the organization, so local deletion does not cause data loss for accounting purposes. This is a pragmatic simplification; a future iteration may introduce soft-deletion or anonymization of payment records instead.
Core Flows¶
Event Eligibility Check¶
Before a user can RSVP or purchase a ticket, they must pass the eligibility pipeline, a series of gates that check membership, questionnaire status, blacklists, capacity, and more.
See Also
The eligibility pipeline is documented in detail in the Architecture section. This section covers only the user-facing flow.
flowchart LR
A[User requests access] --> B{Eligibility Pipeline}
B -->|All gates pass| C[Access granted]
B -->|Gate fails| D[Denied with reason] Questionnaire Submission & Evaluation¶
Some events require users to complete a questionnaire before gaining access.
sequenceDiagram
participant U as User
participant API as API
participant C as Celery
participant SE as SubmissionEvaluator
participant LLM as LLM Provider
U->>API: Submit questionnaire answers
API->>C: Dispatch evaluation task
C->>SE: Evaluate submission
SE->>SE: Calculate score
SE->>LLM: Request AI evaluation (if configured)
LLM-->>SE: Evaluation result
SE-->>SE: Determine outcome
Note over SE: APPROVED / REJECTED / PENDING_REVIEW
SE-->>U: Notification with result The evaluation mode depends on the questionnaire configuration:
| Mode | Behavior |
|---|---|
| Automatic | Score-based with optional LLM evaluation; no human review |
| Manual | All submissions go to PENDING_REVIEW for organizer review |
| Hybrid | LLM evaluates first; edge cases sent to PENDING_REVIEW |
Attendee Flows¶
Browsing Events & Organizations¶
Users browse public events and organizations. Visibility is handled by the for_user queryset manager, which filters based on:
- Event/organization visibility settings (public, unlisted, members-only, private)
- User membership status
- Event publication state (draft events hidden from non-organizers)
Unlisted visibility
Organizations and events set to UNLISTED are accessible via direct link but hidden from browse/search listings. This is useful for soft launches or invite-link-only events.
RSVPing to an Event¶
sequenceDiagram
participant U as User
participant EM as EventManager
participant EP as Eligibility Pipeline
U->>EM: Request RSVP
EM->>EP: Check eligibility
EP-->>EM: Eligible / Not eligible
alt Eligible
EM-->>U: RSVP confirmed
else Not eligible
EM-->>U: Denied with reason
end Getting Tickets¶
The ticket acquisition flow depends on the payment method configured for the ticket tier:
Ticket is issued directly upon request (after eligibility check).
sequenceDiagram
participant U as User
participant API as API
participant S as Stripe
U->>API: Request ticket checkout
API->>S: Create Stripe session
S-->>API: Checkout URL
API-->>U: Redirect to Stripe
U->>S: Complete payment
S->>API: Webhook confirmation
API-->>U: Ticket issued Series Passes (Season Tickets)¶
For event series, attendees can buy a series pass instead of individual tickets: one purchase (pro-rata priced by how many covered events remain) materializes a real ticket for every covered event, all drawn from the same tier capacity as direct sales. The pass has its own QR (series:<uuid>) accepted at check-in for any covered event, plus its own PDF and Apple Wallet downloads.
Joining an Organization (Membership Applications)¶
Users join organizations through the membership-eligibility pipeline:
GET /api/me/organizations/{slug}/join-eligibilitypreviews the outcome — the structurednext_step(submit questionnaire, wait for approval, proceed, …) drives which CTA the frontend renders.POST /api/me/organizations/{slug}/applycreates the application. A gate-free application completes instantly (membership granted); otherwise it stays PENDING and advances every time it is read (GET /api/me/applications/{id}— poll after submitting a questionnaire).- Applications can be cancelled; a rejection allows a fresh re-application.
Private organizations answer 404 on both endpoints (no existence leak). See Membership Eligibility.
Membership Subscriptions (Member)¶
Where an organization sells subscription plans:
- Public plan listings show price, cadence, and availability —
sold_out(cap reached) andsales_status(paused) render distinct states. POST /api/me/organizations/{org_id}/subscribestarts an ONLINE subscription and returns a hosted Stripe Checkoutcheckout_url; the member is redirected there to pay the first invoice (same UX as ticket checkout). Abandoning the redirect is harmless — re-subscribing returns the same open session's URL, or mints a fresh one if it expired.- Members self-serve cancel (immediate or at period end), switch plans (upgrades prorate immediately; downgrades apply at the period boundary), open the Stripe billing portal, and revive a recently expired ONLINE subscription. OFFLINE subscriptions are managed by the organization's staff.
- Failed or SCA-blocked renewals move the subscription to
PAST_DUEand notify the member with a portal link; after the org's grace period it expires with a revival CTA.
Managing Potluck Items¶
For potluck-style events, attendees can:
- View the potluck item list for the event
- Claim items they will bring
- Release items they can no longer bring
- Add custom items to the list
Automatic release
If a user cancels their RSVP or ticket, any potluck items they claimed are automatically released back to the pool.
Organizer Flows¶
Organization Management¶
flowchart TD
A[Create Organization] --> B[Manage Details]
A --> C[Manage Members & Staff]
A --> D[Create Events]
C --> C1[Invite members]
C --> C2[Assign roles]
C --> C3[Remove members]
D --> D1[Configure event settings]
D --> D2[Set up ticket tiers]
D --> D3[Build questionnaires]
D --> D4[Manage invitations] Managing Members & Staff¶
| Action | Required Permission |
|---|---|
| Edit organization details | edit_organization |
| Invite/remove members | manage_members |
| Assign staff roles | manage_members |
| Manage subscription plans & subscriptions | manage_subscriptions |
Membership Subscriptions (Organizer)¶
Organizations can charge recurring membership fees per tier — the "venue card" model. Staff with manage_subscriptions can:
- Create plans on a tier (
Monthly,Annual, …) as OFFLINE (cash/bank transfer, staff-recorded) or ONLINE (Stripe on the org's Connect account). - Bound and pause sales:
max_subscriptionscaps concurrent non-terminal subscriptions (slots free automatically when one cancels or expires);sales_status=PAUSEDstops member self-service sales while staff can still manage subscriptions manually. Archived plans are retired outright. - Drive the lifecycle end-to-end: create OFFLINE subscriptions, record payments, cancel/pause/resume/revive, refund (a full-period refund auto-cancels the subscription), force-migrate subscribers after a price change, and read per-org MRR/churn metrics.
See Membership Subscriptions for the full architecture.
Membership Applications (Organizer)¶
Prospective members apply through the membership-eligibility pipeline (questionnaire and/or manual approval, per-org and per-tier knobs). Staff review pending applications and approve (member materialized at the tier) or reject. See Membership Eligibility.
Invitations¶
Organizers can invite users to events in two ways:
- Direct invitation: User has an account; invitation is linked immediately
- Pending invitation: User does not have an account; invitation is stored by email and linked upon registration. A notification email is sent to the pending invitee with event details.
Organizers can set a custom invitation_message on the event. This message is rendered into every invitation (direct, token claim, and pending conversion) using safe string interpolation with placeholders: {user_name}, {event_name}, {organization_name}, {event_date}. The message is included in invitation emails (HTML and plain text) and Telegram notifications.
Building Questionnaires¶
Organizers create questionnaires with configurable:
- Questions (multiple types: text, choice, scale, etc.)
- Scoring rules per question
- Evaluation mode (automatic, manual, hybrid)
- Pass/fail thresholds
Reviewing Submissions¶
For questionnaires in manual or hybrid mode, organizers review submissions in the admin panel or via the API:
- View pending submissions
- Read answers and AI evaluation (if available)
- Approve or reject each submission
Event Check-In¶
sequenceDiagram
participant S as Staff
participant App as Check-in App
participant API as API
S->>App: Scan attendee QR code
App->>API: POST check-in request
API-->>App: Confirmation + attendee details
App-->>S: Display result Permission Required
Check-in requires the check_in_attendees permission, typically granted to staff members.
Organizer Refunds & Event Cancellation¶
Refunding a payment and cancelling a ticket are orthogonal operations — an organizer can do either independently. Refund money moves through Refund rows (one per attempt); the charge.refunded webhook confirms Stripe's side and never touches ticket status or capacity. See Refunds for the full data-model and webhook detail.
POST /event-admin/{event_id}/tickets/{ticket_id}/refund— refund an online payment, full or partial, without cancelling the ticketGET /event-admin/{event_id}/tickets/{ticket_id}/refund-context— amounts paid/refunded/remaining plus apolicy_suggested_amount(the buyer's own cancellation-policy quote), letting the frontend offer quick-select amounts (full remaining, or the policy-suggested one)POST /event-admin/{event_id}/tickets/{ticket_id}/cancel— cancel a ticket of any payment method, with an optionalrefund_amountthat issues a Stripe refund (online tickets) or records a manual refund note (offline/at-the-door) alongside the cancellation
A Stripe-dashboard refund does not cancel the ticket
Refunding a charge directly from the Stripe dashboard is recorded — matched back to its Payment/Refund rows via the charge.refunded webhook — but it never cancels the ticket or reclaims tier capacity. The organizer must cancel separately if that's the intent.
Cancelling an Entire Event with Refunds¶
PATCH /event-admin/{event_id}/update-status accepts an opt-in refund_tickets flag, valid only together with status=cancelled:
GET /event-admin/{event_id}/cancellation-refund-preview— an advisory pre-flight: total refundable per currency vs. the organization's Stripe balance. It never blocks the cancellation — a shortfall is surfaced (balance_sufficient: false) but the organizer can still proceed.PATCH .../update-statuswithstatus=cancelled, refund_tickets=truestampstickets_refund_started_atand dispatches a background sweep: a parent task fans out one subtask per active ticket, and each subtask refunds (if online) then cancels its ticket independently.- Org staff receive a single summary notification once every ticket has settled (or after a bounded polling window), reporting cancelled / refunded / failed counts and any tickets still not cancelled.
- Irreversible once started. Un-cancelling the event (
update-statusback to a non-cancelled status) aftertickets_refund_started_atis set returns409— resuming a crashed or partial sweep means re-POSTing the same cancel-with-refund call, not un-cancelling. Every per-ticket subtask is idempotent, so re-dispatch safely skips already-cancelled tickets and already-fully-refunded payments.
tickets_refund_started_at is staff-only
This timestamp is exposed only in the cancellation-refund-preview response (gated by the manage_event permission) — it must never appear on public/attendee event schemas.
Additional Flows¶
Apple Wallet Pass¶
After obtaining a ticket, users can add it to Apple Wallet:
- User requests a wallet pass for their ticket via the API
- The system generates a
.pkpassfile containing event details, QR code, and branding - The pass is returned for download and can be added to Apple Wallet
GDPR Data Export¶
Users can request a full export of their personal data:
- User requests data export via
POST /api/account/export-data - A Celery task gathers all personal data (profile, tickets, RSVPs, questionnaire submissions, etc.)
- The export file is made available for download
Organization Following¶
Users can follow organizations and event series to receive notifications about new events:
- Follow/unfollow via the API
- Configure notification preferences (new events, announcements)
- Choose public or private follow visibility
Waitlist¶
When an event or ticket tier reaches capacity:
- User joins the waitlist
- When a spot opens (cancellation or capacity increase), the next waitlisted user is notified
- The user can then complete their RSVP or ticket purchase
Discount Codes¶
Organizers can create discount codes to offer reduced pricing on ticket purchases:
- Create code with type (percentage or fixed amount), value, and optional scope (events, series, tiers)
- Set limits: max uses, per-user limit, minimum purchase amount, validity window
- Buyer enters code during checkout → validated and applied to price
- Usage tracked per code and per user
Duplicate codes & delete-vs-deactivate
Creating a code whose string already exists returns a clear 409 Conflict (it used to surface as an opaque 500). Deleting a code that has never been used hard-deletes it; deleting a code that has been used deactivates it instead, preserving usage history.
Billing Profile Management¶
Users participating in the referral program can set up a billing profile:
| Method | Endpoint | Description |
|---|---|---|
GET | /api/me/billing | Retrieve billing profile |
POST | /api/me/billing | Create billing profile |
PUT | /api/me/billing | Update billing name, address, and email |
DELETE | /api/me/billing | Delete billing profile |
PUT | /api/me/billing/vat-id | Add or update VAT ID |
DELETE | /api/me/billing/vat-id | Remove VAT ID |
Referral payouts
A complete billing profile with self-billing agreement and connected Stripe account is required before referral payouts can be processed.
Bookmarks¶
Users can privately bookmark events for later. Bookmarks do not sign you up and do not affect eligibility.
POST /api/events/{event_id}/bookmark→201for a new bookmark,200if it already existedDELETE /api/events/{event_id}/bookmarkis idempotent and works even for events you can no longer seeis_bookmarkedis reflected on event list/detail responses; find them via thebookmarkedfacet on/dashboard/events(bookmarked unlisted events surface there even though they stay hidden from discovery)
Self-Service Ticket Cancellation & Refunds¶
Opt-in per ticket tier via allow_user_cancellation, cancellation_deadline_hours, and refund_policy (tiered % by hours-before-event + flat fee). Buyers see the cancellation terms before purchase on TicketTierSchema, and the active policy is snapshotted onto the ticket at purchase (refund_policy_snapshot) so later policy changes never affect issued tickets.
GET /events/tickets/{id}/cancellation-preview— policy windows + a live refund quote for this ticketPOST /events/tickets/{id}/cancel— works for free, offline, at-the-door, and online (Stripe) tickets; online tickets trigger a per-Payment, partial-amount-aware Stripe refund- Blocked cases (already cancelled, checked-in, event started, past deadline) return
409 CancellationBlockedErrorSchemawith a stablecode; cancelling an already-cancelled ticket returns 409 - Triggers a
TICKET_CANCELLED/TICKET_REFUNDEDnotification (immediate transactional email) reporting the actualrefund_amount
Recurring Events & Series¶
Organizers can run first-class recurring series: a RecurrenceRule + a template event drive rolling-window materialization, where each occurrence is a real, independent Event. A daily Celery beat task keeps the window populated; template edits propagate to future occurrences via a field whitelist, and a drift endpoint surfaces occurrences that no longer match the rule after a cadence change. A single SERIES_EVENTS_GENERATED digest is sent per generated batch.
- Admin:
POST /organization-admin/{slug}/create-recurring-event, plusPATCH .../event-series/{id}/template(with?propagate=),.../recurrence,POST .../generate/.../pause/.../resume/.../cancel-occurrence, andGET .../event-series/{id}/drift
See Also
Full architecture: Recurring Event Series.
Open-Ended Events¶
An event can be marked is_open_ended for ongoing/no-fixed-end events. end is still set (defaults to start + 24h) and used internally for ICS/Wallet/upcoming gates; the flag is a display signal so the frontend can render "Ongoing" and hide the end time.
Membership Subscriptions¶
Phase 1 — OFFLINE only. Subscriptions are staff-managed (recorded payments), not self-served Stripe billing.
Organizers configure recurring membership plans per membership tier (MembershipSubscriptionPlan), gated by the manage_subscriptions permission (owner ✓, staff ✓ by default, member ✗). Staff create/list/get subscriptions, drive the lifecycle (cancel / pause / resume), and record payments (MembershipPayment, with occurred_at to backfill the real payment date). A post_save signal syncs OrganizationMember.status + tier from the subscription (never creating members, never touching a BANNED member), and a daily beat task expires subscriptions past the org's grace period.
- Member view:
GET /me/membership-subscriptions,GET /me/organizations/{org_id}/subscription, andGET /me/memberships(unified memberships list, inlining the most recent non-terminal subscription per org) - Org policy:
membership_grace_period_days(default 7),membership_refund_policy
Polls¶
Organization-managed polls built on the questionnaire pipeline — a Poll is 1:1 with a Questionnaire and votes are stored as QuestionnaireSubmission rows. Creation and management require the manage_polls permission. Polls have a draft/open/closed lifecycle, configurable audience/result visibility, anonymity, and a vote-change policy.
/api/polls/group: list, detail,GET /{id}/results, create (POST /polls/organizations/{org_id}), patch,open/close/reopen,duplicate, delete (owner-only),POST /{id}/vote,DELETE /{id}/vote
See Also
Full architecture: Polls.
Content Moderation¶
Offensive food-item names are blocked at creation across all supported languages (the moderation app), and staff can delete offensive food items via the admin. Quarantined (malware-flagged) uploads are stored behind a protected, signed-URL media path — never publicly downloadable; staff retrieve them via a signed admin link.
See Also
Full architecture: Content Moderation.
Event Schedule / Timeline¶
Organizers author an ordered list of display-only sessions via PUT /event-admin/{event_id}/schedule. Each session has a title, Markdown description, relative start offset, optional duration/location, and an is_required badge. The schedule is exposed on the public event detail and copied verbatim on event duplication and recurring-series materialization.
Revenue & VAT Reporting¶
A VAT-precise financial-reporting suite for organizers, driven by one tax engine so every view reconciles (online Stripe + offline/at-the-door payments, per currency and per VAT rate; refunds attributed to the period they occurred).
- Live org financials:
GET /organization-admin/{slug}/revenue(sortable, period- and currency-filtered) - Downloadable report:
POST /organization-admin/{slug}/revenue-report→ pollGET .../revenue-reports/{id}; output is a ZIP bundling an XLSX (Summary + Transactions) and a PDF (per-VAT-rate table) - Scheduled delivery: org-level
revenue_report_cadence(NONE/QUARTERLY/MONTHLY, owner-only to change) emails the just-closed period's report to the org billing email and owner - Per-event:
GET /event-admin/{event_id}/tickets/revenue→EventFinancialsSchema
See Also
Full architecture: Billing & VAT.
Guest Checkout & Guest-to-User Upgrade¶
When an event has can_attend_without_login=True, anonymous visitors can attend without an account: a guest RSVP or guest ticket checkout creates a guest RevelUser (guest=True) from just a name and email. If the guest later registers with the same email, an activation link upgrades the guest account to a full user and all previous guest tickets/RSVPs are preserved.
Self-Served Email Change¶
Users can change their own email address:
POST /account/email-change-request— requires the current password; sends a single-use confirmation link to the new address (proof of control)POST /account/email-change-confirm— swapsemail+username, blacklists every outstanding JWT for the user, and returns a fresh token pair so the confirming device stays signed in
Both addresses receive completion emails; the old address also gets an in-flight notice with a masked rendering of the new address. The flow rejects Google-SSO accounts and same-email / already-taken targets with clear 400s, and silently no-ops for globally-banned targets.
Third-Party Apps (OAuth)¶
Revel is an OAuth 2.1 authorization server and OpenID Provider: a third-party app, script or MCP host can act on a user's behalf with a token limited to the scopes that user approved. The whole surface is hidden unless GET /version reports features.oauth_provider. Full developer reference: Third-Party Apps (OAuth). Persona-level journeys for E2E: Journey 28 in USER_JOURNEYS.md.
sequenceDiagram
participant App as Third-party app
participant FE as Revel frontend<br/>/oauth/authorize
participant API as Revel API
App->>FE: redirect with client_id, scope, state, PKCE challenge
FE->>API: GET /api/oauth/authorize?(verbatim query)
API-->>FE: consent description + consent_ticket<br/>(or redirect_to if already granted)
FE->>API: POST /api/oauth/authorize?(same query)<br/>{allow, consent_ticket}
API-->>FE: {redirect_to}
FE->>App: redirect with ?code=…&state=…
App->>API: POST /o/token (code + PKCE verifier)
API-->>App: access token (+ refresh token with offline_access) Connecting an App¶
The user reviews the app (name, logo, homepage, privacy policy, and a warning when Revel has not verified it) and the scopes it asks for, then allows or denies. The consent ticket expires after five minutes; an expired screen is simply shown again. An app the user already approved for the same scopes skips the screen.
Managing Connected Apps¶
Settings → Connected apps (GET /api/oauth/connections/) lists each app with the scopes it holds and when it was last used; Remove (DELETE /api/oauth/connections/{client_id}) kills every token it holds. Changing the account email and a global ban also disconnect apps.
Registering an App (Developers)¶
Settings → Developer apps (/api/oauth/apps/, verified email required): public (PKCE-only) or confidential clients, redirect URIs, and an allowed_scopes ceiling. A confidential client's secret is shown once and can only be rotated. The list shows how many users are connected, never who.
What an app can never do
An app token's power is granted scopes ∩ the user's current permissions, and some routes accept only the user's own login whatever the scopes: buying, RSVPing and other attendee writes, account and security settings, billing, organization finances, Stripe onboarding, staff management and organization invitation links. See What app tokens cannot reach.
Referral Program¶
Referral Flow¶
sequenceDiagram
participant R as Referrer
participant N as New User
participant API as API
participant S as Stripe
R->>N: Shares referral code
N->>API: POST /api/account/register (with referral code)
API->>API: Validate code, create Referral record
Note over API: Revenue share % snapshotted
N->>S: Purchases tickets (over time)
S->>API: Webhook: payment succeeded
API->>API: Platform fee recorded on Payment
Note over API: Monthly (1st): calculate_referral_payouts
API->>API: Aggregate net platform fees per referral
API->>API: Create ReferralPayout (CALCULATED)
Note over API: Monthly (1st, 06:30): process_referral_payouts
API->>API: Generate statement PDF
API->>S: Transfer payout amount
S-->>API: Transfer confirmed
API-->>R: Email with statement PDF Joining the Program¶
Referrers are enrolled through an application (public, gated by SiteSettings.referral_applications_enabled) or an admin invite. Every decision is taken in the Django admin; the frontend only exposes the apply form and the invite prefill on the register page.
sequenceDiagram
participant A as Applicant
participant API as API
participant Adm as Platform admin (Django admin)
A->>API: POST /api/referral/apply {email, code, note}
API-->>A: 202 (also for blocked / already-enrolled emails)
API-->>A: "Application received" email
API-->>Adm: Pushover ping
alt Approve, account exists
Adm->>API: Approve
API->>API: Create ReferralCode, link application
API-->>A: "You're enrolled" email
else Approve, no account yet
Adm->>API: Approve (or Invite by email)
API-->>A: Invite email → /register?referral_invite=<id>
A->>API: Register / SSO login with that email
API->>API: post_save: enroll by email match
API-->>A: "You're enrolled" email
else Reject / Reject permanently
Adm->>API: Reject (+ optional note)
API-->>A: Rejection email (note included when present)
end | Decision | Effect | Applicant sees |
|---|---|---|
| Approve | Enroll now, or invite email with a register link | Enrolled / invite email |
| Reject | Row closed, may apply again | Rejection email (+ note) |
| Reject permanently | Row closed, future applications from that email silently dropped | Same rejection email |
| Invite by email | Pre-approved application created by the admin | Invite or enrolled email |
Guest accounts created by guest checkout are not signups: they are enrolled when the guest becomes a full user (password set or OIDC login). Enrollment never blocks account creation — a code taken in the meantime is logged and left for the admin.
Payout Documents¶
The document type depends on the referrer's billing profile:
| Referrer Type | Document | VAT Treatment |
|---|---|---|
| B2B (validated VAT ID) | Self-billing invoice (Gutschrift) | Full VAT math with reverse charge for EU cross-border |
| B2C (no VAT ID) | Payout statement | No VAT line |
Referral Endpoints¶
| Method | Endpoint | Description |
|---|---|---|
GET | /api/referral/validate?code=... | Validate a referral code |
GET | /api/me | Includes referral_code in response |
GET | /api/me/referral/payouts | List referral payout history |
GET | /api/me/referral/payouts/{id}/statement | Retrieve payout statement details |
GET | /api/me/referral/payouts/{id}/statement/download | Download payout statement PDF |
Attendee Invoicing¶
Organizations can generate invoices for ticket buyers on their behalf. The invoicing mode is configured per organization.
Invoicing Modes¶
| Mode | Invoice Created As | Auto-Emailed | Use Case |
|---|---|---|---|
NONE | — | — | Default (no invoices) |
HYBRID | DRAFT | No — org admin issues manually | Org needs review/control |
AUTO | ISSUED | Yes | Hands-off automation |
VAT Preview¶
Before checkout, the buyer can preview the VAT breakdown based on their billing info:
sequenceDiagram
participant B as Buyer
participant API as VAT Preview Endpoint
participant VIES as VIES (cached)
B->>API: POST billing info + cart items
API->>VIES: Validate buyer VAT ID (Redis cache, 30min TTL)
VIES-->>API: Valid / Invalid
API->>API: Calculate VAT per item
API-->>B: Price breakdown (net, VAT, gross per item) | Buyer Scenario | VAT Treatment |
|---|---|
| Physical event — any buyer (domestic, EU cross-border B2B/B2C, non-EU) | Org's VAT rate — admission is taxed where the event takes place (#868) |
| Virtual event — cross-border EU B2B (validated VAT ID) | Reverse charge (buyer pays net, #869) |
| Virtual event — non-EU buyer | No EU VAT (buyer pays net, #869) |
| Virtual event — domestic buyer | Org's VAT rate (correct: buyer residence = org country) |
| Virtual event — cross-border EU B2C | Org's VAT rate as interim treatment — buyer-country rate legally applies via OSS; flagged with virtual_b2c_disclaimer (#869) |
For physical events a validated VAT ID never discounts the price; it is only printed on B2B invoices.
Invoice Lifecycle (HYBRID Mode)¶
Attendee Invoice Endpoints¶
Buyer-facing¶
| Method | Path | Description |
|---|---|---|
POST | /events/{event_id}/tickets/vat-preview | Preview VAT breakdown |
GET | /dashboard/invoices | List issued invoices |
GET | /dashboard/invoices/{id}/download | Download PDF |
Org admin¶
| Method | Path | Description |
|---|---|---|
PATCH | /organization-admin/{slug}/invoicing | Set invoicing mode |
GET | /organization-admin/{slug}/attendee-invoices | List attendee invoices |
PATCH | /organization-admin/{slug}/attendee-invoices/{id} | Edit draft |
POST | /organization-admin/{slug}/attendee-invoices/{id}/issue | Issue draft |
DELETE | /organization-admin/{slug}/attendee-invoices/{id} | Delete draft |
See also
For full architectural details including credit notes, numbering, and PDF rendering, see Billing & VAT.
EU Country Compliance¶
Revel blocks only the feature a country's law makes non-compliant, only for the sales that law reaches and only from the date it applies:
- online card payment for events held in Italy;
- attendee invoicing for organizers in Croatia, Portugal, Romania, Slovenia, Greece, Hungary and the Basque Country, and in the rest of Spain from 2027-01-01;
- business-buyer invoices in Belgium and Poland.
Austria, Denmark and Poland get non-blocking organizer notices; Croatia, Slovenia, Greece and Hungary add one next to the attendee-invoicing setting, and Spain (Navarre with its own wording) does until its 2027 block starts.
The organization and event-detail responses expose a compliance object, so the UI can hide what would be refused. Refused writes answer 422 {"detail"}.
Every ticket carries the EU fiscal lines: organizer, VAT ID, ticket number, issue time, price and the notice.
Reference: Compliance. Persona-level journeys for E2E: Journey 29 in USER_JOURNEYS.md.