OSubscribe v1.0.0-alpha
A complete recurring-revenue engine for WooCommerce: subscriptions of every cadence, smart dunning that recovers failed charges, save flows that reduce cancels, fixed-installment payment plans, a customer self-service portal, and daily-rollup MRR / churn analytics.
What OSubscribe does
Adds recurring-revenue infrastructure to WooCommerce: subscriptions, billing, dunning, retention flows, payment plans, customer portal, and analytics. Premium plugin, GPL-licensed code, sold once with lifetime updates.
Getting installed
osubscribe folder to /wp-content/plugins/.osub_cron_tick), and sets sensible defaults in osub_settings.Requirements
From zero to first renewal
/?osub_webhook=stripe.osub_subscriptions and schedules the next charge in osub_charges.[osubscribe_account] shortcode on a page to give customers their portal. Wire the page ID into osub_settings.customer_portal_page_id.next_charge_at is due. Or trigger manually with wp osub tick.before_woocommerce_init; it works with both legacy wp_posts orders and the new wc_orders table.Plugin architecture
13 database tables
All prefixed with {prefix}osub_. Created on activation via OSub_DB::activate(). Indexes are tuned for the four hot queries: charges due in next N hours, subs in dunning, subs for a given user, and charges for a given subscription.
| Table | Purpose |
|---|---|
osub_subscriptions | Core subscription record: user, status, cadence, anchor, next_charge_at, gateway, payment_method_id, metadata |
osub_subscription_items | Line items per subscription: product_id, variation_id, quantity, unit_price |
osub_charges | Every charge attempt: scheduled_at, attempted_at, status, gateway_charge_id, idempotency_key (UNIQUE), failure_code, next_retry_at |
osub_dunning_runs | Open dunning runs per failed charge: attempts, outcome (recovering, recovered, exhausted) |
osub_schedule_changes | Audit row for every schedule change: type (pause, resume, swap, cancel), payload, actor |
osub_save_flows | Save flow definitions: name, config JSON, status (draft, active, archived) |
osub_save_flow_runs | One row per offer presented: subscription, flow, reason, offer payload, outcome (accepted, declined, abandoned) |
osub_payment_plans | Fixed-installment plan definitions: name, schedule JSON, total amount, access policy |
osub_metrics_daily | Daily MRR rollup keyed by date: MRR, new MRR, expansion, contraction, churned, recovery counts |
osub_payment_tokens | Tokenized payment methods per user: gateway, token, last4, brand, expiry, is_default |
osub_magic_links | One-tap portal entry tokens: SHA-style token, purpose, payload, expires_at, consumed_at |
osub_audit_log | Every state change per entity: entity_type, entity_id, event, actor, payload |
A 13th table (osub_subscription_items) sits in the same migration to keep multi-line subscriptions cleanly normalized.
Admin interface
A custom top-level menu (osubscribe) with submenus for every operational view. Themed to match OMailer (Inter Tight + JetBrains Mono + Instrument Serif, dark by default with light toggle, orange accent).
| Submenu | Page slug | Purpose |
|---|---|---|
| Dashboard | osubscribe | MRR, active subs, dunning queue depth, recent activity |
| Subscriptions | osub-subscriptions | Searchable subscription list with status filters and bulk actions |
| Renewals | osub-renewals | Upcoming charges grouped by date |
| Dunning | osub-dunning | Open dunning runs, attempt history, manual retry |
| Save Flows | osub-save-flows | Save flow CRUD + presentation analytics |
| Payment Plans | osub-payment-plans | Fixed-installment plan CRUD + outstanding A/R |
| Reports | osub-reports | MRR, churn, recovery, payment plan revenue |
| Migration | osub-migration | Importer for legacy WooCommerce Subscriptions |
| Integrations | osub-integrations | Wired hooks into OMailer, OLoyalty, OIntel |
| Settings | osub-settings | All plugin configuration (single osub_settings option) |
Settings reference
All settings stored in wp_options under key osub_settings. Defaults set on first activation; merge-safe on upgrade.
Currency & portal
| Key | Type | Description |
|---|---|---|
currency | string | 3-letter ISO. Defaults to woocommerce_currency. |
customer_portal_page_id | int | WP page ID hosting [osubscribe_account] shortcode. |
Dunning
| Key | Default | Description |
|---|---|---|
dunning_max_retries | 4 | Max retry attempts before marking exhausted. |
dunning_retry_schedule | [3, 5, 7, 14] | Days between attempts. Filterable per subscription via osub/dunning/retry_schedule. |
Retention & analytics
| Key | Default | Description |
|---|---|---|
enable_save_flows | true | Show save flow offers on cancel. |
enable_payment_plans | true | Allow products to be sold as payment plans. |
mrr_compute_daily | true | Run the daily metrics rollup on cron. |
Subscription lifecycle
Subscriptions move through a small, well-defined state machine. Every transition fires osub/subscription/transitioned with the from + to state, plus a state-specific action.
| Status | Meaning | Transitions to |
|---|---|---|
pending | Created but not yet billed | active, cancelled |
trialing | Inside the trial window | active, cancelled |
active | Billing on schedule | past_due, paused, cancelled |
past_due | Last charge failed; in dunning | active, cancelled |
paused | Customer-requested pause; no billing | active, cancelled |
cancelled | Final state; no further billing | (terminal) |
PHP// Lifecycle hooks fire alongside the generic transitioned event.
do_action( 'osub/subscription/created', $sub_id );
do_action( 'osub/subscription/renewed', $sub_id, $charge_id );
do_action( 'osub/subscription/paused', $sub_id );
do_action( 'osub/subscription/resumed', $sub_id );
do_action( 'osub/subscription/cancelled', $sub_id, $reason );
do_action( 'osub/subscription/transitioned', $sub_id, $from, $to );
Billing engine
The cron tick runs every five minutes. It selects subscriptions where next_charge_at ≤ NOW() in batches of 50 (filterable via osub/scheduler/batch_size) and dispatches each to its gateway with a unique idempotency key.
osub_cron_tick) on a 5-minute schedule.status = active and next_charge_at in the past, ordered by oldest due first.osub_charges with a generated idempotency_key (UNIQUE constraint).osub/charge/succeeded fires, the subscription’s next_charge_at is advanced, billings_completed is incremented.osub/charge/failed fires, dunning enrollment kicks in.osub/charge/should_attempt to short-circuit a charge attempt (e.g. skip during a billing freeze window). Returning false postpones the charge by 24 hours.Dunning engine
When a charge fails, OSubscribe enters the subscription into a dunning run (row in osub_dunning_runs) and schedules retries on a configurable schedule. Default is 4 attempts at 3, 5, 7, and 14 days.
Run outcomes
| Outcome | Meaning |
|---|---|
recovering | Open run; attempts are still scheduled |
recovered | A retry succeeded; subscription returns to active |
exhausted | All attempts failed; subscription transitions to cancelled |
Customer-facing email cadence
- On first failure: card-update email with magic-link to portal
- On attempt 3: reminder email with explicit copy about coming cancellation
- On exhaustion: cancellation confirmation with reactivation link
PHP// Per-subscription retry schedule override
add_filter( 'osub/dunning/retry_schedule', function( $schedule, $subscription ) {
if ( $subscription->amount_recurring() > 100 ) {
return [ 2, 5, 9, 14, 21 ];
}
return $schedule;
}, 10, 2 );
// Outcome listeners
add_action( 'osub/dunning/recovered', function( $sub_id, $attempts ) { /* notify */ }, 10, 2 );
add_action( 'osub/dunning/exhausted', function( $sub_id, $last_failure ) { /* notify */ }, 10, 2 );
Save flows
When a customer hits cancel in the portal, OSubscribe presents the first eligible save flow offer. Each presentation is logged to osub_save_flow_runs with reason, offer payload, and outcome.
| Outcome | Meaning |
|---|---|
accepted | Customer accepted the offer; subscription stays |
declined | Customer rejected and proceeded to cancel |
abandoned | Customer closed the dialog without choosing |
PHPuse Orravo\OSub\SaveFlows\Engine;
Engine::register([
'name' => 'Pause instead of cancel',
'eligible' => fn( $subscription ) =>
$subscription->interval_unit() === 'month'
&& $subscription->billings_completed() >= 3,
'offer' => [
'type' => 'pause',
'duration_days' => 60,
'headline' => 'Take a 60-day break instead?',
],
]);
Payment plans
Sell a fixed-price item over a finite number of installments. Unlike a subscription, a plan has a known total and a clear end date. Access policy can revoke content if the customer defaults.
PHPuse Orravo\OSub\PaymentPlans\Plan;
Plan::save([
'name' => 'Annual Course, 4 payments',
'total_amount' => 480.00,
'currency' => 'USD',
'schedule' => [
[ 'amount' => 120, 'days_after_signup' => 0 ],
[ 'amount' => 120, 'days_after_signup' => 30 ],
[ 'amount' => 120, 'days_after_signup' => 60 ],
[ 'amount' => 120, 'days_after_signup' => 90 ],
],
'access_policy' => [ 'revoke_on_default' => true ],
]);
MRR analytics
A daily cron job rolls every active subscription and charge into osub_metrics_daily (one row per date). The Reports admin page reads only this table, so the dashboards are fast even on stores with many subscribers.
| Column | Meaning |
|---|---|
mrr | Total monthly recurring revenue at end of day |
new_mrr | MRR added by new subscriptions today |
expansion_mrr | MRR added by upgrades |
contraction_mrr | MRR removed by downgrades |
churned_mrr | MRR removed by cancellations |
active_subscribers | Count of active subs at end of day |
dunning_recoveries | Number of dunning runs closed as recovered |
dunning_recoveries_amt | Dollar amount recovered today |
Rebuild the entire history from raw events with wp osub metrics rebuild --from=2026-01-01. Useful after schema migrations or backfills.
Self-service portal
A single shortcode ([osubscribe_account]) renders the customer’s portal: active subscriptions, billing history, payment methods, save flow entry. Requires the user to be logged in.
- Pause / resume: customer-initiated; logs to
osub_schedule_changes - Swap payment method: tokenized card update via the gateway’s SetupIntent flow
- Cancel: triggers any eligible save flow before completing
- Download invoice: per-charge PDF rendered from a template
?osub_magic={token} in support emails. The token is consumed on click and the customer lands inside the portal without a password reset round-trip.WooCommerce integration
- HPOS-compatible: declares
custom_order_tablescompatibility onbefore_woocommerce_init - Subscription product type: registered alongside Simple, Variable, Grouped
- WooCommerce My Account: a Subscriptions tab is added under
/my-account/ - Order linkage: every charge can produce a paid WooCommerce order (
parent_order_idon the subscription,order_idon the charge) - Coupons: Woo coupons applied at checkout flow through to the recurring price (configurable: first cycle only vs. forever)
- Importer:
OSub_Migrationreads existing WooCommerce Subscriptions data and back-fillsosub_subscriptions+osub_charges
Payment gateways
| Gateway | Adapter | Webhook endpoint |
|---|---|---|
| Stripe | OSub\Gateways\Stripe | /?osub_webhook=stripe |
| PayPal | OSub\Gateways\PayPal | /?osub_webhook=paypal |
| Offline | OSub\Gateways\Offline | (none; manual reconciliation) |
Register additional gateways through osub/gateways/register. The contract is small: implement charge( $subscription, $idempotency_key ) and verify_webhook( $request ).
Shortcodes
[osubscribe_account]
REST API endpoints
Base namespace: /wp-json/osub/v1/. Admin endpoints require osub_manage capability; /me/ endpoints require login.
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /subscriptions | List subscriptions with status, gateway, search filters |
| GET | /subscriptions/{id} | Single subscription with items, charges, schedule changes |
| POST | /subscriptions/{id}/pause | Pause a subscription |
| POST | /subscriptions/{id}/resume | Resume a paused subscription |
| POST | /subscriptions/{id}/cancel | Cancel; triggers save flow if eligible |
| GET | /me/subscriptions | The logged-in customer’s subscriptions |
| POST | /me/subscriptions/{id}/swap_method | Customer self-service payment method swap |
| GET | /charges | List charges with status filter and date range |
| GET | /reports/summary | MRR, churn, recovery summary card |
| GET | /reports/mrr | Daily MRR series for charting |
| GET | /dunning/queue | Open dunning runs sorted by next retry |
| POST | /webhooks/{gateway} | Inbound gateway webhook receiver |
Developer hooks
Action hooks
PHP// Bootstrap
do_action( 'osub/booted' );
do_action( 'osub/event', $event_name, $payload );
do_action( 'osub/event/' . $event_name, $payload );
// Subscription lifecycle
do_action( 'osub/subscription/created', $sub_id );
do_action( 'osub/subscription/renewed', $sub_id, $charge_id );
do_action( 'osub/subscription/paused', $sub_id );
do_action( 'osub/subscription/resumed', $sub_id );
do_action( 'osub/subscription/cancelled', $sub_id, $reason );
do_action( 'osub/subscription/transitioned', $sub_id, $from, $to );
// Charges
do_action( 'osub/charge/attempted', $sub_id, $charge );
do_action( 'osub/charge/succeeded', $sub_id, $charge );
do_action( 'osub/charge/failed', $sub_id, $charge );
// Dunning
do_action( 'osub/dunning/entered', $sub_id, $run_id );
do_action( 'osub/dunning/recovered', $sub_id, $attempts );
do_action( 'osub/dunning/exhausted', $sub_id, $last_failure );
// Save flows
do_action( 'osub/save_flow/offered', $sub_id, $flow_id, $offer );
do_action( 'osub/save_flow/accepted', $sub_id, $flow_id );
do_action( 'osub/save_flow/declined', $sub_id, $flow_id );
// Payment methods + magic links
do_action( 'osub/payment_method/updated', $user_id, $token_id );
do_action( 'osub/magic_link/consumed', $user_id, $token, $purpose );
// Webhooks
do_action( 'osub/webhook/stripe', $event );
do_action( 'osub/webhook/paypal', $event );
// Gateway registration extension point
do_action( 'osub/gateways/register', $registry );
Filters
PHP// Skip a charge attempt entirely (postpones by 24 hours)
apply_filters( 'osub/charge/should_attempt', $bool, $subscription );
// Per-subscription retry schedule override
apply_filters( 'osub/dunning/retry_schedule', $schedule, $subscription );
// Cron tick batch size
apply_filters( 'osub/scheduler/batch_size', 50 );
WP-CLI surface
SHELL# Subscription operations
wp osub subscription show 1842
wp osub subscription pause 1842
wp osub subscription resume 1842
# Charge operations
wp osub charge retry 7321 --force
# Dunning
wp osub dunning queue
# Metrics
wp osub metrics rebuild --from=2026-01-01
# Migration (legacy WC Subscriptions)
wp osub migrate dry-run
wp osub migrate run --batch=200
# Demo / testing
wp osub seed-demo
wp osub tick
Versus the alternatives
| Feature | WC Subscriptions | Stripe Subs | SubscriptionPro | OSubscribe |
|---|---|---|---|---|
| Smart dunning | basic | basic | ✓ | ✓ |
| Customer portal | add-on | hosted | limited | ✓ |
| Save flows | no | no | add-on | ✓ |
| Payment plans | add-on | no | add-on | ✓ |
| MRR / churn analytics | no | in dashboard | add-on | ✓ |
| Magic-link portal entry | no | no | no | ✓ |
| Idempotency keys per charge | no | ✓ | no | ✓ |
| HPOS compatibility | no | ✓ | partial | ✓ |
| Pricing | $249/yr | $10/mo + 0.5% | $199/yr | $99 once |
Pricing model
One purchase, three tier sizes, lifetime updates. No subscriber-count tax. No per-charge fees. Buy once, install on the sites in your tier, get every future update free.
- 1 production WooCommerce store
- All 13 tables, all 22 features
- Lifetime updates
- Email support · 48h
- Up to 10 production sites
- Importer for legacy WC Subscriptions
- Priority support · 24h
- Free onboarding call
- Unlimited production sites
- Custom gateway integration sprint
- Webhook + analytics starter pack
- Priority support · same-day
Changelog
- Initial public alpha
- 13 database tables shipped:
subscriptions,subscription_items,charges,dunning_runs,schedule_changes,save_flows,save_flow_runs,payment_plans,metrics_daily,payment_tokens,magic_links,audit_log - Stripe + PayPal + offline gateway adapters
- Smart dunning engine with filterable retry schedule
- Save flow runtime + admin CRUD
- Payment plan engine with access policy
- Daily MRR / churn rollup table
- Customer portal shortcode
[osubscribe_account] - Magic-link tokens for one-tap portal entry
- WP-CLI surface (
wp osub subscription/charge/dunning/metrics/migrate/seed-demo/tick) - HPOS compatibility declared
- WooCommerce subscription product type
- REST API under
/wp-json/osub/v1/ - Audit log table for every state change
- Idempotency keys on every charge
Got a question about OSubscribe?
Reach out directly. Kenneth replies within 24 hours.
