Skip to content

Running the platform

Payments

Players pay for bookings, entries and fees through a payment gateway: Stripe, NETOPIA Payments or PayPal, each switched on in the admin with its keys in .env.local. Stripe also pays venues out, renews memberships and serves the app's payment sheet; NETOPIA and PayPal take payments on the web.

Stripe

  1. In the Stripe dashboard, under Developers, copy the secret and publishable keys into .env.local:

    STRIPE_SECRET_KEY=sk_live_...
    STRIPE_PUBLISHABLE_KEY=pk_live_...
    
  2. Add a webhook endpoint at https://<your site>/api/v1/payments/webhook/stripe for these events: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, checkout.session.expired, payment_intent.succeeded, payment_intent.payment_failed, refund.created and refund.updated. Copy its signing secret:

    STRIPE_WEBHOOK_SECRET=whsec_...
    
  3. In the admin, under Settings, Payments, switch on "Take card payments through Stripe".

The keys are yours: the platform never sees them anywhere but your server. Empty keys keep Stripe off, whatever the setting says.

NETOPIA Payments

For Romanian installs. In NETOPIA's admin, take the API key, the point of sale's signature and NETOPIA's public key:

NETOPIA_API_KEY=...
NETOPIA_POS_SIGNATURE=XXXX-XXXX-XXXX-XXXX-XXXX
NETOPIA_PUBLIC_KEY=...   # the PEM, or it base64-encoded

Switch on "Take cards through NETOPIA Payments" under Settings, Payments. Its payment notifications come to https://<your site>/api/v1/payments/webhook/netopia (sent with each payment, nothing to set up), signed by NETOPIA and answered as it expects. Refund NETOPIA payments in NETOPIA's dashboard: its credit notification refunds the fee here.

PayPal

Create a REST app in PayPal's developer dashboard, and a webhook on it at https://<your site>/api/v1/payments/webhook/paypal for PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.DENIED and PAYMENT.CAPTURE.REFUNDED:

PAYPAL_CLIENT_ID=...
PAYPAL_SECRET=...
PAYPAL_WEBHOOK_ID=...

Switch on "Take PayPal" under Settings, Payments. A payment is captured when the payer comes back from PayPal (the website or app sends the return to /api/v1/payments/{id}/return); PayPal verifies its own webhooks.

"Test mode for NETOPIA and PayPal" uses their sandboxes, with no real money.

Apple Pay and Google Pay

The app pays through Stripe's payment sheet, which offers Apple Pay and Google Pay beside the cards when they're switched on in the Stripe dashboard (Settings, Payment methods). On the web, Stripe Checkout offers them too; Apple Pay on the web also needs your domain verified in the dashboard.

Fees

What players owe comes from the competition, the venue and staff:

  • Match fees, on a competition (Fees): a fee for each player picked for a match, starters and bench, and a fee per side for the match, paid by its captain or shared equally by the players picked. They're asked once the captain confirms the team sheet, and follow it: a player taken off the sheet no longer owes theirs, and a shared fee is shared again among those who haven't paid, so the side never pays more than its fee.
  • Entry fees, on a competition: an entry's place is held until its fee is paid, and it plays once it is. Staff can give it a place to let it play before; the fee is still owed.
  • Court bookings: the booker pays the price. At a venue paid when booking (the venue's Court booking settings), with card payments on, a booking holds the court for 30 minutes while it's paid, and frees it if the payment doesn't come. Elsewhere it's paid later, online or at the venue.
  • Fines: staff issue them under Payments, Fees, to a player or a team (its captain pays).

Unpaid fees are reminded from a day before they're due, once a day, three times at most, by app:matchday:run.

Much of grassroots sport still pays at the venue. A fee paid by hand is marked paid, with how (cash or bank transfer) and an optional note such as the transfer's reference:

  • a side's captain marks their players' match fees and the side's own, from the team sheet in the app;
  • a venue's manager marks a booking paid at the desk (Court bookings, "Paid at the venue");
  • an admin marks any fee (Payments, Fees, "Mark paid").

Its reminders stop, an unpaid entry plays and a held booking stands, and the audit log keeps who said so. The Fees page filters by how a fee was paid, so payments by hand show apart from those online.

Refunds

Money comes back without anyone chasing it:

  • A match called off (Fixtures, "Call off", or its status edited to cancelled): what's owed for it isn't any more, and every fee paid for it comes back. Both sides are told, and so is each payer.
  • A dropout, by the competition's rule (Fees): they get it back, get it back once replaced (the side still has a full team without them), or don't.
  • A cancelled booking, by the venue's court booking rules: everything in time or when staff cancel, else the venue's late refund percentage.
  • Staff refund part or all of any fee (Payments, Fees, "Refund").

Paid online, it goes back through the gateway that took it: all of it includes any card fee paid on top. A refund made in the gateway's own dashboard reaches the fee too. Paid by hand, it's owed back by hand: the fee shows "refund due" until someone gives it back and says so ("Given back").

Card processing fees

Under Settings, Payments, choose whether the card processing fee is absorbed (the fee is what the payer pays) or added on top. Added on top, the payer pays enough more that what's left after the gateway's percentage and fixed fee is the fee: with 1.5% and 0.25, a 5.00 fee is charged 5.33.

Receipts and VAT

Every payment gets a receipt, by card, in cash or by transfer, emailed to the payer in their language (with Settings, Mail & notifications, Send emails switched on). Receipts are numbered in the install's series: an optional prefix, the year, and a number that restarts each year (INV-2026-000123).

Under Settings, Invoices, set the business name, address and VAT number printed on receipts, the number prefix, and a VAT rate for each kind of payment: match fees, a side's fees, entry fees, court bookings, fines and memberships. Prices include VAT: where a rate is set, the receipt shows the net and the VAT in the price.

Payments, Receipts lists them all, and exports a month as a CSV for your accountant.

Memberships

Venues and clubs sell memberships under Payments, Membership plans: a price per month or per year. A venue's members book its courts at member prices for as long as their membership runs.

  • By card or direct debit: the member joins in the app or on the website and pays the first period at Stripe's page, which sets up the renewals. Stripe charges each period after by itself, until the member cancels (they stay a member until the period paid for ends). Direct debit (SEPA) appears there when it's switched on in the Stripe dashboard (Settings, Payment methods). Add these events to the webhook endpoint: invoice.paid, invoice.payment_failed and customer.subscription.deleted.
  • A failed renewal keeps the member a member while Stripe tries again, and asks them to update their payment method, which opens Stripe's own page for it.
  • By hand: staff record a membership paid at the desk under Payments, Memberships: it runs for the plan's period and doesn't renew by itself.
  • Reminders: a yearly renewal is announced a week ahead, and a membership that won't renew is reminded to renew a week before it ends (app:matchday:run). Monthly renewals renew quietly.
  • A team's captains see which of their players are members of the team's club.

Payouts to venues and clubs (Stripe Connect)

What a venue earns through the platform reaches its own Stripe account, with your commission taken on the way: the venue's own rate, or the default under Settings, Payments. In a league whose clubs collect their own match fees (the competition's Fees), a side's match fees go to its club the same way, at the default rate.

  1. In the Stripe dashboard, switch on Connect, with Express accounts.

  2. Add a Connect webhook endpoint at https://<your site>/api/v1/payments/webhook/stripe/accounts for account.updated, and copy its signing secret:

    STRIPE_CONNECT_WEBHOOK_SECRET=whsec_...
    
  3. A venue's manager (or you) opens the venue in the admin and chooses "Connect payouts". Stripe asks for the venue's details and bank account in its own pages: none of it passes through, or is kept by, the platform. Back in the admin, "Check payouts" shows how the account stands; the webhook keeps it up to date.

How the money moves:

  • Paid by card: the platform takes the payment. Your commission and Stripe's fee come off, and the rest is transferred to the venue's account (the venue pays Stripe's fee). Until the venue's account takes transfers, its share is held, and transferred as soon as it does (app:matchday:run tries again too).
  • Paid by hand: the venue holds the money and owes your commission, collected through its monthly statement.
  • Refunds: your commission comes back in proportion, and the venue's share is reversed by as much. Stripe doesn't give back its fee on a refund: the venue owes that, on its statement.

Payments, Statements shows each month, venue by venue: earnings by card and by hand, the commission taken, Stripe's fees, what was transferred or is held, refunds, and what each venue owes you. A venue's manager sees their own venues.

How a payment is applied

  • An order is paid once a gateway's verified webhook (or the checkout's return) says so, for exactly the order's amount and currency. Anything else is logged and refused.
  • Each event is applied once: the same webhook sent again, or a later event for a payment already settled, changes nothing.
  • A paid order is fulfilled through Messenger (the booking confirmed, the entry accepted, the fee marked paid), once.
  • A refund goes back through the gateway that took the payment, in part or in full.

Found a mistake? Tell us.