Full-featured membership and subscription management plugin for Craft CMS 5. Stripe and PayPal integration, tiered access control, content gating, drip content, and a complete subscription lifecycle -- built for the Craft ecosystem.
- Stripe & PayPal -- Recurring payments via Stripe Checkout Sessions and PayPal Subscriptions API v2
- Membership Plans -- Tiered plans with configurable billing intervals (day/week/month/year), trial periods, and per-plan pricing
- Content Gating -- Restrict entries by section, entry type, category, or individual entry with redirect, paywall, or hide behaviors
- Drip Content -- Schedule content to unlock N days after subscription start
- User Group Sync -- Automatically add/remove users from Craft user groups based on subscription status
- Coupons & Discounts -- Percentage or flat-rate codes synced to Stripe Coupons
- Member Portal -- Self-service subscription management via Stripe Customer Portal
- Reporting -- MRR, churn rate, trial conversion, growth metrics, and dashboard widgets
- Transactional Emails -- Welcome, receipt, payment failed, trial ending, cancellation, and drip unlock notifications via Craft's mailer
- REST API -- JSON endpoints for plans, subscriptions, checkout, portal, and member info with API key auth
- Twig Extension --
craft.headcountvariable and{% headcountGate %}tag for template-level gating - CLI Commands --
headcount/subscriptions/expire,headcount/subscriptions/sync,headcount/sync/plans,headcount/sync/status
- Craft CMS 5.0+
- PHP 8.2+
- Stripe account (for Stripe payments)
- PayPal Business account (optional, for PayPal payments)
composer require justinholtweb/craft-headcount
php craft plugin/install headcountOr install from the Craft Plugin Store.
Navigate to Headcount > Settings and enter your Stripe API keys. Optionally enable PayPal.
Settings can be overridden via config/headcount.php:
<?php
return [
'stripeSecretKey' => getenv('STRIPE_SECRET_KEY'),
'stripePublishableKey' => getenv('STRIPE_PUBLISHABLE_KEY'),
'stripeWebhookSecret' => getenv('STRIPE_WEBHOOK_SECRET'),
'paypalClientId' => getenv('PAYPAL_CLIENT_ID'),
'paypalClientSecret' => getenv('PAYPAL_CLIENT_SECRET'),
'paypalEnabled' => false,
'defaultCurrency' => 'USD',
'checkoutSuccessUrl' => '/membership/thank-you',
'checkoutCancelUrl' => '/membership/plans',
'loginUrl' => '/login',
'pricingUrl' => '/membership/plans',
];Go to Headcount > Plans and create membership tiers. Each plan maps to:
- A Stripe Price (auto-created on first sync, or enter an existing Price ID)
- A Craft User Group (members are automatically added/removed)
- A billing interval, price, and optional trial period
Point your Stripe webhook to:
https://yoursite.com/actions/headcount/webhook/stripe
Required Stripe events:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paidinvoice.payment_failedcustomer.subscription.trial_will_end
For PayPal:
https://yoursite.com/actions/headcount/webhook/paypal
Go to Headcount > Access Rules to gate content. Rules can target a section, entry type, category, or individual entry. Choose a behavior:
| Behavior | Effect |
|---|---|
| Redirect | 302 redirect to login or pricing page |
| Paywall | Show truncated teaser content with a CTA |
| Hide | Return 404 for unauthorized users |
{# Pricing page #}
{% for plan in craft.headcount.plans() %}
<div class="plan">
<h3>{{ plan.name }}</h3>
<p>{{ plan.currency|upper }} {{ plan.price|number_format(2) }}/{{ plan.billingInterval }}</p>
{% if plan.features %}
<ul>
{% for feature in plan.features %}
<li>{{ feature }}</li>
{% endfor %}
</ul>
{% endif %}
<form method="post">
{{ csrfInput() }}
<input type="hidden" name="action" value="headcount/checkout/create-session">
<input type="hidden" name="planHandle" value="{{ plan.handle }}">
<button type="submit">Subscribe</button>
</form>
</div>
{% endfor %}{# Check if current user has any active subscription #}
{% if craft.headcount.isSubscribed() %}
{# Check specific plan #}
{% if craft.headcount.isSubscribed('pro-monthly') %}
{# Check if user can access an entry (respects gating + drip) #}
{% if craft.headcount.canAccess(entry) %}
{# Get current user's active subscriptions #}
{% set subs = craft.headcount.subscriptions() %}
{# Get all enabled plans #}
{% set plans = craft.headcount.plans() %}
{# Get a specific plan #}
{% set pro = craft.headcount.plan('pro-monthly') %}
{# Checkout URL (form POST is preferred, but this works for links) #}
{{ craft.headcount.checkoutUrl('pro-monthly') }}
{# Customer Portal URL #}
{{ craft.headcount.portalUrl() }}
{# Drip content checks #}
{% if craft.headcount.isUnlocked(entry) %}
{{ craft.headcount.unlocksIn(entry) }} {# days remaining #}
{# Coupon input field helper #}
{{ craft.headcount.couponField() }}
{# Raw subscription query #}
{% set query = craft.headcount.subscriptionQuery() %}{# Gate content to any active subscriber #}
{% headcountGate %}
<p>Members-only content here.</p>
{% endheadcountGate %}
{# Gate to a specific plan #}
{% headcountGate planHandle='pro-monthly' %}
<p>Pro content here.</p>
{% endheadcountGate %}Since Headcount syncs subscriptions to Craft user groups, you can also use native Craft checks:
{% if currentUser and currentUser.isInGroup('pro') %}
<p>Pro member content</p>
{% endif %}All endpoints are prefixed with /actions/headcount/api/.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /plans |
Public | List all enabled plans |
| GET | /plan?handle=xxx |
Public | Get a specific plan |
| GET | /subscriptions |
Session/API key | List current user's subscriptions |
| GET | /subscription?id=xxx |
Session/API key | Get a specific subscription |
| POST | /checkout |
Session | Create a checkout session |
| GET | /portal |
Session | Get Stripe Customer Portal URL |
| GET | /member |
Session/API key | Get current member info with subscriptions |
Authentication: Session-based for logged-in users, or pass an API key via X-Headcount-Api-Key header or ?apiKey= query parameter.
# Process expired subscriptions (cancel those past period end)
php craft headcount/subscriptions/expire
# Sync all active subscription statuses from Stripe/PayPal
php craft headcount/subscriptions/sync
# Sync plans to payment providers (create Stripe Products/Prices)
php craft headcount/sync/plans
# Check sync health and display stats
php craft headcount/sync/statusHeadcount fires events you can listen to in custom modules or plugins:
use justinholtweb\headcount\services\Subscriptions;
use justinholtweb\headcount\events\SubscriptionEvent;
use yii\base\Event;
Event::on(
Subscriptions::class,
Subscriptions::EVENT_AFTER_CREATE_SUBSCRIPTION,
function (SubscriptionEvent $event) {
$subscription = $event->subscription;
// Custom logic after subscription creation
}
);Available events on Subscriptions service:
EVENT_BEFORE_CREATE_SUBSCRIPTION/EVENT_AFTER_CREATE_SUBSCRIPTIONEVENT_BEFORE_UPDATE_SUBSCRIPTION/EVENT_AFTER_UPDATE_SUBSCRIPTIONEVENT_BEFORE_CANCEL_SUBSCRIPTION/EVENT_AFTER_CANCEL_SUBSCRIPTION
Configure a webhook URL in Headcount > Settings to receive POST notifications for subscription lifecycle events. Payloads are signed with HMAC-SHA256 via the X-Headcount-Signature header.
Events: subscription.created, subscription.updated, subscription.canceled, subscription.expired, member.upgraded, member.downgraded
Headcount uses a hybrid architecture:
- Subscription Element -- A custom Craft element type that stores billing state (gateway IDs, status, dates, amounts) in the
headcount_subscriptionstable linked to theelementstable - User Group Sync -- Active subscriptions automatically add users to the plan's mapped Craft user group; cancellation/expiration removes them
- Content Gating -- Hooks into
Entry::EVENT_AUTHORIZE_VIEWat the system level, so gating works regardless of template code - Payment Gateways -- Stripe uses the
stripe/stripe-phpSDK with theStripeClientinstance pattern; PayPal uses the REST API v2 directly via Guzzle
| Table | Purpose |
|---|---|
headcount_plans |
Membership plan definitions |
headcount_subscriptions |
Subscription element content (FK to elements) |
headcount_access_rules |
Content gating rules |
headcount_drip_schedules |
Drip content timing |
headcount_coupons |
Discount codes |
headcount_webhook_logs |
Incoming webhook event log (idempotency) |