Build WooCommerce Subscriptions

KINDAreplaces $23.25/mosaves $279/yrback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A small set of plans sold through hosted checkout, webhooks as the only writer of entitlement, renewals that handle an authentication requirement by asking the customer rather than failing silently, a dunning sequence with a grace period, and self-service cancellation that does not need you.

estimated effort weekendthe files for this build are in the project pack

RuntimePHP with WordPress, or any small serverPaymentsHosted checkout with saved payment methods and webhooksMoneyInteger minor unitsEntitlementOne table consulted by every gateEmailA transactional provider with a verified domain

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree if you already have one

    Why This is billing logic bolted to something that already exists. Building a store first is a different project.

    Get it Use the store you already run, or a small application with user accounts. What matters is that there is something to gate.

    Verify A gated page exists that you can protect

  2. API keyfree in test mode; live billing costs per transaction plus a further fee on subscriptions in most regions

    Why Hosted checkout, saved cards, subscription events and the authentication flow all come from here.

    Get it Sign up, create prices for your plans, take the secret key and the webhook signing secret.

    Verify A triggered subscription event reaches your endpoint and is verified

  3. have readyfree

    Why Phase 3 is the whole point of this build and it cannot be tested with a card that always succeeds.

    Get it Your provider publishes test card numbers including ones that always require authentication and ones that fail on renewal. Find them before Phase 3.

    Verify A renewal produced a requires-action state in test mode

  4. API keyfree tiers cover this volume

    Why The authentication link and the dunning sequence both depend on delivery, and one in spam is a lost customer.

    Get it Create an account, add the DNS records they specify, take the credentials.

    Verify A test email arrives at three providers and is not in spam

  5. decidefree

    Why How long a customer keeps access after a failed payment is a commercial decision rather than a technical one, and it should be decided before it is implemented.

    Get it Decide the grace period, the number of retries and when access ends. Write it down and put it in your terms.

    Verify The policy is written and matches the configuration

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- plans(id, name, price_minor, currency, interval, provider_price_id, active)
- customers(id, email, provider_customer_id, created_at)
- subscriptions(id, customer_id, plan_id, provider_subscription_id, state, current_period_end, cancel_at_period_end, grace_until)
- invoices(id, subscription_id, provider_invoice_id, amount_minor, state, attempted_at, authentication_url, paid_at)
- entitlements(customer_id, plan_id, granted_at, revoked_at, reason) · the only thing any access check reads
- webhook_events(provider_event_id, type, processed_at) · idempotency, because events are redelivered
- State is derived from invoices and entitlements rather than set by hand, so the database cannot disagree with itself.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
STRIPE_SECRET_KEYsecretrequiredsk_test_...Stay in test mode until reconciliation and the authentication path are both proved.
STRIPE_WEBHOOK_SECRETsecretrequiredwhsec_...From the registered endpoint or the listen command.
GRACE_PERIOD_DAYSrequired7How long access persists after a failed or pending renewal. A commercial decision, stated in your terms.
DUNNING_ATTEMPTSrequired4Retry attempts before access ends. Bounded, with one email each.
EMAIL_FROMrequiredbilling@example.comOn the verified sending domain, since billing email that lands in spam costs customers.
ACCOUNT_PORTAL_URLrequiredhttps://example.com/accountWhere customers manage and cancel without contacting you.

The build, in order

  1. Entitlement as the single gate

    Every access check reads one table, before any money is involved.

    1. Customer and plan to a boolean with a reason. Nothing else decides access anywhere.

      Files src/access.php

    2. Routes, not templates. A hidden link is not a gate.

      Files src/gates.php

    3. Walk the routes, request each with and without entitlement, assert both outcomes.

      Files tests/GatesTest.php

    done when · tick each as it passes
    watch out
    • Checking a subscription's state at each gate spreads billing logic through the application, and the first time a state is added, half the gates are wrong. One entitlement table, one function, and subscriptions write to it rather than being consulted.
  2. First payment and the webhook

    Someone can subscribe, and only the webhook grants access.

    1. Prices from your own table. Never an amount supplied by the client.

      Files src/checkout.php

    2. Signature verified, event identifiers recorded, entitlements written there and nowhere else.

      Files src/webhook.php

    3. The thank-you page may load first. Say so gracefully rather than showing no access.

      Files src/return.php

    done when · tick each as it passes
    watch out
    • Granting on the success redirect hands out access to anyone who can replay a URL, and the customer may close the tab before it loads anyway. The webhook is the truth and the redirect is a thank-you page that must handle arriving before the entitlement exists.
  3. The renewal that needs the customer

    A renewal requiring authentication asks the customer instead of failing.

    1. It is a normal outcome, not an error. Store the authentication link from the invoice.

      Files src/renewals.php

    2. The customer cannot act on something they were not told about.

      Files src/dunning.php

    3. Providers offer test cards that always require it. Use one rather than hoping.

    done when · tick each as it passes
    watch out
    • Treating a payment that requires action as a failure and revoking access immediately punishes a customer for their bank's prompt, which they have not seen because they were not present. Expect this state as routine, keep access, ask them, and give them time.
  4. Dunning without nagging

    A failed payment is retried and communicated a fixed number of times, then resolved.

    1. A fixed number of attempts over a fixed window. Unbounded retries are just noise.

      Files src/dunning.php

    2. At send time, not at queue time. This is what prevents chasing a paid customer.

      Files src/dunning.php

    3. So access ending is not the first the customer hears of it.

      Files src/dunning.php

    done when · tick each as it passes
    watch out
    • Continuing to email after a customer has paid or cancelled is the failure people remember and complain about publicly. Check the current state before every send rather than trusting the schedule that queued it, since the state may have changed between queuing and sending.
  5. Cancellation, upgrades and the money in between

    A customer can change or cancel without you, and the amounts are right.

    1. Through the provider's portal or your own page. Requiring an email is a dark pattern.

      Files src/account.php

    2. And compare against the provider's figure. A mismatch is a bug rather than rounding.

      Files src/proration.php

    3. Amount and date. It is the question every subscriber has.

      Files src/account.php

    done when · tick each as it passes
    watch out
    • Proration arithmetic in floating point produces amounts that disagree with the provider's by a penny, and reconciling that at scale is miserable. Compute in integer minor units and reconcile against the provider's own figure rather than trusting yours.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more commerce.