phpwordpresswoocommercestripe-connectcompliance

Ticket Marketplace

Peer-to-peer ticket resale for WordPress. Escrow-backed payouts, a per-market compliance engine, and ticket files that live outside the web root.

published
August 2, 2026
read
5 min
words
914
stack
5

Overview

Fans list tickets they can't use, buyers purchase them, and the money stays in the platform balance until the day after the event.

Built on WooCommerce, which owns cart, checkout, tax, order state and refunds. The plugin owns events, listings, ticket-file custody, escrow release, fraud screening, commission, and the compliance engine. Feature-complete against WordPress 7.0 and WooCommerce 10.9, with 381 automated checks across three suites.

How the money moves

Stripe Connect separate charges and transfers. The buyer's payment lands in the platform balance at checkout, and no transfer exists until the release job creates one a day after the event ends.

checkout ──▶ payout row (pending) ──▶ event ends ──▶ +24h ──▶ Transfer to seller
                   │
                   ├── dispute opened ──▶ frozen, never releases
                   ├── dispute upheld ──▶ refunded, net zeroed
                   └── order refunded ──▶ refunded, stock returned

Two alternatives were rejected. Stripe's delay_days on the connected account caps at 90 days, and festival tickets sell further out than that. funds_segregation sounds purpose-built but is preview-only, available in ten countries, and explicitly incompatible with cross-border payouts, which a worldwide marketplace needs.

Holding funds is the load-bearing decision here. A pre-release refund costs only the processing fee, so buyer protection is cheap and every other choice leans on it.

The limit worth understanding

TicketSwap and Tixel are trusted because a sold ticket's original barcode gets invalidated and a fresh one issued. That needs an integration with whoever sold the ticket first, and those APIs are gated to approved partners.

This does file handoff instead, which carries no cryptographic guarantee. A seller can upload the same PDF twice. Four things compensate, and none of them is a substitute:

  1. Moderation. Every listing is reviewed before sale. No product exists until approval, so an unreviewed ticket can't be bought even through a direct add-to-cart URL.
  2. Duplicate detection. A barcode hash with a unique index on (event_id, barcode_hash), plus a file-content hash for tickets whose barcode won't extract.
  3. Seller verification. Stripe Connect Express KYC before a first listing goes live.
  4. Post-event payouts. An invalid ticket surfaces at the door while the platform still holds the money.

The barcode extractor reads PDF content streams and returns null rather than guessing. Photographs and screenshots always return null, because decoding a QR from an image needs a computer-vision dependency and pretending otherwise would give false confidence. Nulls go to manual review, flagged so the reviewer knows automatic detection didn't apply.

Compliance

Rules resolve against the event's country rather than the seller's or the buyer's. A London show is governed by UK law whoever sells the ticket. The default for an unresearched market is a face-value cap rather than free pricing, because a worldwide marketplace will meet countries nobody has reviewed and a cap is the safe failure mode.

MarketRule
BEblocked — bans habitual resale and "provision of means" for it
GBface value, seat disclosure, 4 listings per event
IEface value — Sale of Tickets Act 2021
AU110% of face value, seat disclosure
USuncapped — no federal cap
defaultface value

Enforcement happens at four points: listing creation, listing edit, moderator approval, and again at checkout. A festival listing can sit live for months while the rule table changes underneath it, so nothing is trusted once written.

These positions move. The UK bill and the EU Digital Fairness Act were both in flight when this was written, which is why every rule carries a note field explaining what it was reacting to.

Ticket file custody

Ticket files are never WooCommerce downloadables. Woo grants download access on order status alone, and a ticket also requires that the listing cleared moderation.

Files live outside the web root. The first implementation kept them under uploads/ behind an .htaccess deny, and testing over HTTP on nginx showed every ticket PDF was downloadable by URL with no login. .htaccess is Apache and LiteSpeed only, web.config is IIS only, and nginx honours neither. Nothing but being outside the served tree protects these files.

Delivery goes through an HMAC-signed URL valid for 15 minutes, compared with hash_equals, with entitlement decided per request. Unauthenticated, tampered-signature, expired, wrong-context and no-entitlement requests each return 403, verified over real HTTP.

Details that took a second pass

Commission is basis points, not float percentages. 8.25% is 825, exact through every calculation. 0.0825 is not.

The percentage floors and net is derived by subtraction, so commission plus net always equals gross. Rounding both halves independently is how marketplaces lose a cent on every payout.

The rate is frozen onto the payout row at the moment of sale. Recomputing at payout time would let an admin editing the default change what a seller is owed for a sale that already happened.

Null means inherit; zero means zero. A negotiated 0% rate is a real arrangement, so it can't collapse into "unset".

Listings are a custom table, not a CPT. Transactional rows filtered hard on event, status and price, which postmeta handles badly. Money is stored in integer minor units so no float rounding reaches an amount a seller is paid.

The WooCommerce product is a one-way projection. Product stock is mirrored for early feedback, but authority sits in a single conditional UPDATE. Two simultaneous checkouts can both pass a Woo stock check and oversell a ticket that exists exactly once.

Payout creation is idempotent, enforced by a unique index on order_item_id. WooCommerce fires status transitions more than once, and paying a seller twice isn't recoverable by a retry.

Active theme: bosco, dark mode.