Paid listings
How to sell membership tiers and promotions: prices in config/paidlistings.yaml, Stripe Checkout or staff invoices, the portal's billing screen, the Stripe webhook, orders and their statuses, and how a paid order grants, renews, refunds and lapses a membership or a promotion.
engine-dmo
Paid listings takes payment for what a site sells its partners: a membership tier for a period, and promotions in its priced slots. A partner pays in the portal through the provider’s hosted checkout (Stripe Checkout is the one provider), or staff raise an invoice and record its payment. Either way the result is an order, and a paid order is fulfilled: a tier order makes the organisation’s membership active at that tier until the end of the period it bought, and a promotion order approves the promotion. A refund takes it back, and a tier order’s membership lapses when its period ends.
Without config/paidlistings.yaml, or with provider: none, nothing is sold in the portal; staff can still raise invoices for priced promotions.
Configuration
provider: stripe
stripe:
secret_key: ${STRIPE_SECRET_KEY}
webhook_secret: ${STRIPE_WEBHOOK_SECRET}
sites:
main:
promotions: checkout
tiers:
standard: { amount: "120.00", currency: GBP, months: 12 }
gold: { amount: "300.00", months: 12 } | Key | Default | Meaning |
|---|---|---|
provider | none | stripe takes payment through Stripe Checkout; none keeps orders to staff invoices |
stripe.secret_key | Required for stripe. Must be a ${VAR} interpolation; a literal key is refused. | |
stripe.webhook_secret | Required for stripe. The webhook endpoint’s signing secret, as a ${VAR}. | |
stripe.api_base | https://api.stripe.com | Only for a test double: an https address, or http on a loopback address |
sites.<site>.tiers.<tier>.amount | The price, a quoted decimal such as "120.00" | |
sites.<site>.tiers.<tier>.currency | the site’s default_currency | A three-letter code |
sites.<site>.tiers.<tier>.months | 12 | The period bought, 1 to 36 months |
sites.<site>.promotions | invoice | checkout: a priced promotion request is paid in the portal. invoice: staff invoice it. checkout needs a provider. |
A tier on sale must be declared in config/memberships.yaml and sold on the site (memberships.tiers in site.yaml). A promotion’s price comes from its slot in promotions.yaml (see Promotions), never from this file.
engine-dmo check validates the file without needing the secrets; the server reads the two variables at start and refuses to start when either is unset. A key beginning sk_live_ or rk_live_ is live mode; any other is test mode.
Setting up Stripe
Create a restricted or secret API key in the Stripe dashboard and set it as STRIPE_SECRET_KEY in the engine’s environment.
Add a webhook endpoint in Stripe at https://<site host>/paidlistings/webhook/stripe, listening for checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, checkout.session.expired and charge.refunded. Set its signing secret as STRIPE_WEBHOOK_SECRET.
Write config/paidlistings.yaml, run engine-dmo check, and restart the server.
The engine creates one Checkout Session in payment mode per order, for one line at the order’s amount, naming the order as the client reference and in the payment’s metadata. It never sees card details.
The webhook
POST /paidlistings/webhook/stripe checks the Stripe-Signature header and its age (at most 5 minutes) before reading the body, refuses an event it has already applied (event ids are kept 90 days), holds a payment’s amount to the order’s, and then fulfils the order. A delivery that fails its signature counts against its address’s allowance and is answered 429 past it; one that verifies is never limited.
| Stripe event | Effect |
|---|---|
checkout.session.completed with payment status paid, checkout.session.async_payment_succeeded | The order is paid and fulfilled |
checkout.session.expired, checkout.session.async_payment_failed | The open order is cancelled |
charge.refunded | When the charge is refunded in full, the order is refunded and what it granted is taken back. A partial refund changes nothing; record it from the Orders screen if it should. |
The billing screen
The portal’s billing screen, {portal path}/billing/{org}, is for people whose partner role grants billing.pay (the built-in billing and owner roles). It shows:
- the organisation’s membership on the site;
- with a provider, each tier on sale with its price and period;
- on a site with
promotions: checkout, its promotion requests waiting for payment, priced from their slot; - its orders and their statuses.
Each offer is a plain form that posts to {portal path}/billing/{org}/checkout, which records the order at the price in configuration (the form only names the offer) and answers 303 to the provider’s checkout page. It works without JavaScript. The screen renders the portal template billing.html, which engine-dmo portal scaffold --site <site> copies for restyling.
A promotion request on a checkout site is requested until the partner starts checkout, then awaiting_payment while its order is open; payment approves it, with the slot’s capacity checked again. If the order is cancelled or abandoned, the promotion goes back to requested.
Orders
| Property | Meaning |
|---|---|
| Kind | tier or promotion |
| Method | checkout (paid through the provider) or invoice (raised and recorded by staff) |
| Amount, currency, description | Fixed when the order is made |
| Reference | The order’s reference, written on the membership or promotion it fulfils |
| Status | Meaning | Moves to |
|---|---|---|
open | Made, not yet paid | paid, cancelled |
cancelled | Abandoned or cancelled | paid (a late payment) |
paid | Payment received, not yet fulfilled | fulfilled, failed, refunded |
failed | Fulfilment could not go ahead (for example the slot filled meanwhile); staff retry or refund | fulfilled, refunded |
fulfilled | The tier or promotion was granted | refunded, lapsed |
lapsed | A tier order’s period ended | refunded |
refunded | Refunded; what it granted was taken back |
stateDiagram-v2 [*] --> open : checkout started or invoice raised open --> paid : payment received open --> cancelled : abandoned or cancelled cancelled --> paid : a late payment paid --> fulfilled : tier or promotion granted paid --> failed : fulfilment could not go ahead paid --> refunded failed --> fulfilled : staff retry failed --> refunded fulfilled --> lapsed : a tier order's period ends fulfilled --> refunded lapsed --> refunded refunded --> [*]
What fulfilment does
| Order | Fulfilled | Refunded | Period ends |
|---|---|---|---|
| Tier | The organisation’s membership on the site becomes active at the tier, renewing on the period’s end. With no current membership one is created, joined today. A renewal of an active membership at the same tier runs on from its current renewal date. The membership records the amount, currency and order reference. | The membership lapses, if it still carries this order’s reference | The membership lapses, unless a later order or staff renewed it under another reference |
| Promotion | The promotion is approved (capacity checked again with the slot locked) | The promotion is cancelled with the reason the business reads |
Paid orders whose fulfilment was interrupted are retried every minute; ended periods are lapsed every hour, by the site’s timezone.
Staff
Staff with paidlistings.manage on the site (the built-in administrator and partnership-manager roles) use the Orders screen under Members to:
- raise an invoice for a tier on sale or for a promotion waiting for payment;
- record an invoice’s payment, which fulfils it;
- cancel an open order;
- retry a failed fulfilment;
- record a refund made in Stripe or in the finance system.
Troubleshooting
| Message | Fix |
|---|---|
stripe.secret_key is a secret and must be an environment interpolation, not a literal | Write secret_key: ${STRIPE_SECRET_KEY}. |
provider "..." is not one of stripe, none | Use stripe or none. |
promotions: checkout needs a payment provider | Set provider: stripe, or promotions: invoice. |
site <site> does not sell tier "<tier>" | Add the tier to the site’s memberships.tiers. |
a tier on sale needs the install's tier list | Declare tiers in config/memberships.yaml. |
months <n> is not between 1 and 36 | Fix months. |
The server will not start: environment variable STRIPE_SECRET_KEY (stripe.secret_key) is not set | Set both variables in the environment. |
Affiliates
How a site earns as an affiliate publisher: programmes with networks or direct deals, tracked links under /go/, the disclosure templates print, importing conversions by CSV, network connectors or a signed postback, the postback's signature and body, and the affiliate commands.
Multisite and networks
How several sites in one install share or separate partners: networks of sites that share organisations, people and portal accounts, what stays per site (tiers, slots, portals, landing pages), how staff are scoped to a network, and moving a site out with site export and retire.