Building a Pretix + Stripe Connect Plugin for Live-Music Venues

Igor Ganapolsky · June 5, 2026 · 5 min read

Facts verified against Upwork contract history + commit log. No metric stated below was invented; if it isn't measured, it isn't here.

The Brief

A regional live-music operator (Hilltown Media Group) needed to ship venue-branded online ticketing on top of Pretix — a self-hosted, open-source event ticketing platform — with a multi-venue Stripe Connect topology where each venue stays Merchant of Record (MoR) for tax purposes. They came in with a clear architectural opinion: Direct Charges (or Destination with on_behalf_of), a fixed $1.00 platform fee decoupled from the venue's sales tax, and a Pretix-plugin-shaped extension surface so nothing forks the upstream platform.

What Was Delivered or Funded (Phase 1 Evidence Gates)

The Upwork contract has three Phase 1 milestones with separate evidence gates. I am keeping the accounting language strict here:

The verified cash gate as of June 5, 2026 is M1 withdrawn at $1,350 net, M2 pending at $900 net, and M3 escrow funded at $1,500 gross. Phase 2 ($5,000) covers pre-tab drink tokens, featured-merch upsells at checkout, an automated waitlist with SetupIntent -> off-session PaymentIntent conversion, on-door upsell modals on the Pretix scan API, and Stripe Terminal JS/RN integration for the box office.

$1,350
M1 Net Withdrawn
$900
M2 Net Pending
$1,500
M3 Gross Escrow
$5,000
Phase 2 Budget

Three Architectural Decisions Worth Lifting

These are the choices that took the most thinking and that I'd reuse on the next marketplace platform — listed not because they're novel but because each one quietly avoids a class of bug.

1. application_fee_amount instead of cart-line platform fee

The Problem: The naive way to charge a per-ticket platform fee is to add a line item to the cart. That's wrong on a marketplace where each venue is its own MoR — the fee shows up in the venue's taxable revenue, and the venue's accountant has to back it out manually.

Using Stripe Connect's application_fee_amount keeps the fee on the platform's Stripe account, off the venue's books, and out of every tax jurisdiction's audit surface. Two-line change. The reason to know about it isn't the code, it's the months of back-office pain it removes.

2. Native Pretix add-on items, not custom cart math

The Problem: Phase 2 needs to inject upsells (drink tokens, featured merch) into the buyer flow. The naive way is a parallel cart that re-totals at checkout. That breaks every downstream Pretix invariant: refund math, partial-refund signals, organizer reporting, tax calculation.

Modeling the upsells as native Pretix Item add-ons means the existing Phase 1 Stripe Direct Charge math calculates the new totals correctly with zero changes. Same applies for refunds — Pretix's order_canceled / order_refunded signals already know how to attribute partial refunds back to add-on items.

The general principle: when extending a platform with a plugin model, lean on the platform's native primitives even when they feel verbose. The downstream-invariant cost of going around them is paid forever.

3. SetupIntent for the waitlist, not stored card-on-file

The Problem: Phase 2's "never lose a sale" waitlist needs to charge the next buyer when a ticket is returned — possibly hours or days after they joined the waitlist. The naive way is to store a PaymentMethod against the user record.

The right way is SetupIntent → vault setup_intent_id only (not the PaymentMethod, not last4) → convert to off-session PaymentIntent when a seat frees. PCI scope stays SAQ-A. And the off-session path forces you to handle authentication_required declines (5-15% expected on EU/UK SCA + US 3DS2) with a skip-and-notify fallback — which means the waitlist actually keeps moving instead of stalling on one issuer step-up.

What I'd tell the next freelance dev shipping a Pretix-plus-Stripe-Connect project

Pretix Primitives: Read the Pretix signals.py source before you write your own. Half the work is already there.
Webhook Idempotency: Webhook reconciliation isn't optional. Build the StripeWebhookEvent table (event-id PK for idempotency) on day one. It's the dashboard you'll want at 11pm when a venue's first show is selling.
Stripe Terminal Connect: Stripe Terminal on Connect is a multi-week integration, not a multi-day one. Reader Location registration alone is its own onboarding flow.

What's Next

Phase 2 ($5K, milestone-scoped — upsells, automated waitlist, box office POS, and webhook reconciliation), then Phase 3 (gamification + SMS marketing) and Phase 4 (self-serve onboarding + PWA scanner).

— Igor


More build logs: From git init to production · The agent harness pattern · MCP pre-action checks explained