Plugins Documentation How It Works About Support My Account Browse Plugins

Bodholdt Licensing Docs

Set up your self-hosted licensing system from scratch. Full integration guide included.

Full Guide
v10.28.0

What Bodholdt Licensing is

Bodholdt Licensing turns your own WordPress site into a complete store and licensing server for the plugins and themes you sell. Customers buy through Stripe, get a license key by branded email, download your software, and manage their own subscriptions — all on your site, with no third-party SaaS in the middle and no WooCommerce required. Each piece of software you ship phones home to your server to check its license and pull updates. This page is written for two audiences: shop owners who just want to get selling, and developers who need the exact API, database, and SDK details — both are covered.

Quick start (about 10 minutes)

  1. Install & activate. Upload bodholdt-licensing.zip via Plugins → Add New → Upload Plugin, then activate.
  2. Follow the Setup Wizard. Go to Bodholdt Licensing → Dashboard and the wizard walks you through each step below.
  3. Enter your license key. Paste the key from your purchase email. You have a 7-day grace period before a key is required.
  4. Connect Stripe. Enter your Stripe Secret Key (sk_live_...) and Webhook Secret (whsec_...).
  5. Add your products. Go to Settings → Products and register each plugin or theme you want to sell.
  6. Generate your pages. Click Generate Pages to auto-create your Shop, Success, and Portal pages.
  7. Upload your zip files. Use the upload button in the Product Manager, or drop .zip files into the vault directory shown in Settings.
  8. Generate your SDK. Go to SDK Generator, enter your plugin details, and copy the generated licensing.php file into your plugin.

That’s it. Your customers can now buy licenses, receive keys by branded HTML email, download your software, and manage their subscriptions — all from your own site.

What you need first

A few things have to be in place before the plugin can do its job:

Requirement Minimum
WordPress 6.0 or higher
PHP 8.0 or higher
SSL HTTPS required
Stripe account stripe.com
SMTP plugin Recommended (WP Mail SMTP, FluentSMTP, etc.)
Server Apache or Nginx (Nginx needs a manual vault deny rule — see Downloads)

The plugin checks whether an SMTP plugin is active and shows a warning on the Dashboard if your email delivery might be unreliable. If you see that warning, install one of the SMTP plugins above so your customers actually receive their license keys.

Connecting Stripe

Stripe handles the money. You give the plugin two things from Stripe — a Secret Key (so it can create checkouts) and a Webhook Secret (so Stripe can tell your site when a purchase happens).

Step 1: Add your API key

  1. Log in to your Stripe Dashboard.
  2. Go to Developers → API Keys.
  3. Copy your Secret Key (sk_live_...) and paste it into Bodholdt Licensing → Settings → Stripe & License.
  4. Click Verify to confirm the connection.

Step 2: Set up the webhook

A webhook is just a message Stripe sends to your site the moment something happens (a sale, a cancellation, a failed payment). Set it up once:

  1. In Stripe, go to Developers → Webhooks.
  2. Click Add Endpoint.
  3. Set the URL to exactly: https://yoursite.com/?cls_webhook=stripe
  4. Select these four events:
    • checkout.session.completed — processes new purchases and generates licenses
    • customer.subscription.updated — syncs cancellations, reactivations, and payment-status changes
    • customer.subscription.deleted — immediately expires licenses
    • invoice.payment_failed — logs the failure and notifies the admin
  5. Copy the Signing Secret (whsec_...) and paste it into your Settings.

You don’t have to worry about duplicate sales: webhook processing has idempotency protection (transient-based locking prevents the same purchase from creating two licenses) and Stripe signature verification on every event.

Adding your products

Each plugin or theme you sell is a product. Set them up under Settings → Products.

The fields you fill in

Field What it’s for
Slug Unique identifier (e.g. my-plugin). Must match the item_reference in your SDK code.
Name Display name shown to customers at checkout.
Filename Exact name of the .zip file in your vault (e.g. my-plugin.zip).
Monthly / Yearly / Lifetime Price amount and Stripe Price ID (price_xxx) for each billing cycle. Leave blank to disable a cycle.
Trial Days Number of free-trial days (0 to disable). Limited to one trial per product per email.
Max Domains Maximum sites a single license key can activate on (default: 1).

Getting your Stripe Price IDs

Each price you charge lives in Stripe and has its own ID. To create one:

  1. In Stripe, go to Products → Add Product.
  2. Add a Recurring price for monthly/yearly, or a One-time price for lifetime.
  3. Copy the Price ID (price_1Abc...) into the matching field in the Product Manager.
  4. Click the Validate button next to each Price ID field to check it against Stripe.

Releasing updates (the auto-updater)

When you ship a new version, your customers can get it through the normal WordPress update screen. To push an update, set the version metadata under Settings → Products → Product Versions:

  1. Upload the new .zip to your vault (same filename, or update the filename in Product Manager).
  2. Update the Version number in Product Versions.
  3. Optionally update the Changelog, Tested WP, Requires WP, and Requires PHP fields.

Every customer installation will see the update within 12 hours — or right away if they click Check Again under Dashboard → Updates.

Uploading your files

Upload .zip files (up to 100MB) straight from the Product Manager with the upload button. They’re stored in the vault directory set in Settings. On Apache, a .htaccess deny rule is created for you automatically. On Nginx you have to add the deny rule by hand — the Dashboard shows you the exact rule to paste when it detects Nginx (see Secure download system below).

The three customer pages (shortcodes)

Clicking Generate Pages in the Product Manager creates all three of these for you automatically. If you ever need to place one by hand, drop the shortcode onto a page. (Shown here as text so they don’t run.)

[cls_checkout_form]

This is your storefront. It renders a full shopping cart and checkout flow: customers pick products, choose a billing plan (monthly / yearly / lifetime / trial), see live price calculations, confirm their order, enter their email, and get sent to Stripe Checkout. It supports the ?buy=slug URL parameter to pre-select a product.

The form handles mixed carts (paid products plus free trials in one checkout), prevents mixing monthly and yearly subscriptions in the same cart (a Stripe limitation), and enforces one trial per product per email. If you haven’t configured any products, it shows “Store Coming Soon.” If your own plugin license is invalid, it shows “Store Temporarily Unavailable.”

[cls_success_page]

The thank-you page customers land on after paying. It reads the session_id query parameter to look up the completed checkout and shows:

  • License keys for each purchased product
  • Download buttons (submitted via POST so keys never appear in the URL or server logs)
  • “What’s Next” instructions for using the SDK
  • Links to the customer portal and shop

[cls_portal]

A self-service portal for your customers, with two features:

  • Manage Billing — the customer enters their email and gets a link to the Stripe Billing Portal, where they can update payment methods, view invoices, and cancel subscriptions.
  • My Keys — the customer enters their email, receives a 6-digit verification code, then sees all their active license keys with download buttons. Verification codes expire after 10 minutes.

No WordPress user accounts are needed — everything runs on email verification.

The SDK Generator (developer reference)

Plain version: this builds the little file you drop into each plugin so it can talk to your licensing server. No hand-coding required. The details below are precise so developers can rely on them.

How to use it

  1. Go to Bodholdt Licensing → SDK Generator.
  2. Enter your Plugin Name and a unique Prefix (letters/numbers only).
  3. Click Generate SDK.
  4. Copy the generated code and save it as licensing.php in your plugin directory.
  5. Add this to your main plugin file: require_once __DIR__ . '/licensing.php';

What the SDK includes

  • License verification — checks the key against your server every 12 hours.
  • Domain activation / deactivation — registers and releases domain slots with your license server.
  • Auto-updater — hooks into pre_set_site_transient_update_plugins so customers see native WordPress update notifications. Downloads are authenticated via HTTP header (the license key never appears in a URL or server log).
  • Admin license page — a styled settings page where customers enter and manage their license key (activate, deactivate, re-verify).
  • Feature gating — use YourPrefix_Licensing::is_licensed() anywhere in your code to gate premium features.
  • Admin notice — displays a dismissible notice when the plugin is unlicensed.
  • Plugin info modal — provides changelog, requirements, and author info in the WordPress “View Details” popup.

Managing licenses

Every license you’ve issued lives under Bodholdt Licensing → Manage Licenses. From there you can do everything by hand if you ever need to.

Things you can do to a license

Action What it does
Issue New License Manually create a license for any product and email address.
Quick Create Create a license straight from the Dashboard with email, product, and optional expiry.
Revoke Immediately block a license. The customer loses access.
Reactivate Restore a revoked license to active status.
Delete Permanently remove a license record.
Sync Pull the latest subscription status from Stripe for a specific license.
Bulk Actions Revoke, reactivate, or delete multiple licenses at once (uses database transactions for atomicity).
Send Email Resend the license-key email to the customer.

Editing in place

  • Domains — click the × next to any registered domain to remove it from a license.
  • Expiry — click the expiry date to edit it inline with a date picker.

Finding and exporting

Search by email, license key, or product. Filter by status (Active, Blocked, Expired). Choose how many show per page (10, 25, 50, 100). Export all licenses as CSV.

The Dashboard & Setup Wizard

The Dashboard (Bodholdt Licensing → Dashboard) is your home base. It gives you:

  • Metrics cards — total licenses, active, expired, trials, and products (cached with a 5-minute transient).
  • Setup checklist — tracks completion of your plugin license, Stripe keys, webhook, storage path, products, pages, and email delivery.
  • Setup wizard — a multi-step walkthrough for license key entry, Stripe configuration, and page generation.
  • Recent activity — the latest license events from the activity log.
  • Quick create — issue a new license without leaving the Dashboard.
  • Quick links — jump to common destinations (checkout page, portal, Stripe Dashboard).

The Gatekeeper (your own license protection)

The Gatekeeper protects your Bodholdt Licensing installation — it makes sure your own license with Bodholdt Labs is active. Here’s how it behaves, in plain terms:

  • Grace period: you have 7 days after activation to enter your license key.
  • Daily verification: the plugin phones home to bodholdtlabs.com once a day to verify your license.
  • Graceful degradation: if your server can’t reach the home server, your license status is not changed. A 30-day stale check makes sure prolonged network blocks or firewall rules eventually trigger re-verification.
  • Soft lockout: if your license expires, creating new licenses and running new checkouts is disabled. Existing customer licenses, downloads, activations, and subscription management all keep working normally.

The important part: your customers are never affected by your license status.

Activity Log

Every license event is recorded under Bodholdt Licensing → Activity Log — a running history you can search and export.

What gets tracked: created, activated, deactivated, revoked, reactivated, deleted, payment failed, reminder sent, email failed.

  • Filter by action type and search by key, details, or IP address.
  • IP tracking with Cloudflare-aware detection (HTTP_CF_CONNECTING_IP).
  • Automatic daily rotation purges entries older than the configured retention period (default: 365 days, minimum: 30).
  • Manual purge: choose 7, 30, 60, 90, 180, or 365 days.
  • CSV export — download up to 10,000 entries as CSV.
  • GDPR/CCPA export — export all license and activity data for a specific customer email as JSON.

Emails to your customers

Bodholdt Licensing sends professional, branded HTML emails — a gradient header, a white content area, and a branded footer. You can customize the templates under Settings → Emails. Use Send Test Email in that tab to preview how your templates render before going live.

When emails go out

Email When it’s sent Customizable?
Trial License After a free-trial checkout (no payment) Yes — subject and body template
Purchase License After a successful Stripe payment (webhook) Yes — subject and body template
Expiry Reminder At 30, 7, and 4 days before expiration (cron) No — auto-generated
Trial Upgrade 4 days before trial expiration (cron) No — includes upgrade CTA
Payment Failed On the invoice.payment_failed webhook No — admin notification
Verification Code Customer requests a portal key lookup No — 6-digit code, 10-min expiry
Billing Portal Link Customer requests billing-portal access No — contains the Stripe Portal URL
Manual License Email Admin clicks “Send to Customer” No — auto-generated

Merge tags

Drop these into the Trial and Purchase email templates and the plugin fills them in:

Tag Replaced with
{licenses} Formatted license keys with product names, expiry dates, and download buttons
{email} The customer’s email address
{manage_url} Link to the customer portal page

Secure download system

Plain version: customers can only download your files with a valid, active license key, and the files themselves can never be reached directly by URL. The mechanics below are exact for developers and for setting up Nginx.

Downloads are handled by a priority-1 init hook that runs before any other WordPress processing. The flow:

  1. The customer submits a download request with their license key (via POST form, GET parameter, or the X-License-Key HTTP header).
  2. The plugin validates the key: it exists, its status is active/valid, and it’s not expired.
  3. Rate-limit check: 10 downloads per minute per IP.
  4. The file is streamed in 8KB chunks with output buffering and gzip disabled for clean delivery.
  5. Connection status is checked after each chunk — streaming stops if the client disconnects.

Vault configuration

  • Preferred location: outside the web root (e.g. /bodholdt_product_vault/ one level above public_html). The plugin creates this automatically.
  • Fallback location: inside wp-content/uploads/ with a randomized directory name.
  • Apache: a .htaccess file with Deny from all is generated automatically on activation.
  • Nginx: add a deny rule manually. The Dashboard shows the exact rule when it detects Nginx:
    location ~* /wp-content/uploads/plugin_products/ {
        deny all;
    }

The auto-updater in the generated SDK uses the X-License-Key HTTP header to authenticate downloads, so license keys never appear in URLs or server access logs.

Moving settings between sites

If you run a staging and a production site, you can copy your settings between them under Settings → Advanced.

  • Export: downloads a JSON file with all product configs, email templates, page slugs, currency, and brand settings.
  • Import: upload a JSON file to restore those settings.
  • Security: Stripe API keys, webhook secrets, operating mode, and license keys are never exported or imported. The import uses an allowlist to block injection of sensitive values. Enter credentials manually on each server.

Advanced settings

Found under Settings → Advanced:

Setting What it does
Brand Name Replace “Bodholdt Licensing” with your own brand name throughout the admin and emails.
Checkout Button Text Customize the “Add Product” button label on the checkout form.
Payment Failure Notifications Turn admin email alerts on or off when a subscription payment fails.
Preserve Data on Delete When enabled, uninstalling the plugin keeps all database tables, options, and transients intact.

API Reference (developer reference)

This section is for developers integrating with the licensing server directly. The generated SDK uses these endpoints under the hood; the values below are exact.

Query-string API (legacy)

Used by the SDK for license verification and auto-updates. No authentication required — the license key acts as the credential. All endpoints are rate-limited (20/min per IP).

Endpoint Parameters Response
?cls_action=cls_check license_key, item_reference, registered_domain License status, expiry, domain count
?cls_action=cls_activate license_key, item_reference, registered_domain Registers the domain against the license
?cls_action=cls_deactivate license_key, item_reference, registered_domain Releases the domain slot
?cls_action=update_check item_reference Latest version, changelog, requirements (5-min cache)
?cls_action=download license_key (GET, POST, or X-License-Key header) Streams the zip file

REST API

Full REST API at /wp-json/bodholdt-licensing/v1/. Admin endpoints require authentication with the manage_options capability. Public endpoints are rate-limited (30/min per IP).

Method Endpoint Auth Description
GET /licenses Admin List all licenses (paginated). Args: page, per_page, search, status
POST /licenses Admin Create a license. Args: product, email, expiry, is_trial
GET /licenses/{key} Public Get license details. Public: minimal data. Admin: full data (email, domains, txn_id)
POST /licenses/{key}/activate Public Activate license on a domain. Args: domain
POST /licenses/{key}/deactivate Public Deactivate license from a domain. Args: domain
GET /products Public List all products with pricing info
GET /update-check/{slug} Public Check for plugin updates by product slug

Rate limiting

All public-facing endpoints are rate-limited using WordPress transients with per-IP tracking. The plugin trusts HTTP_CF_CONNECTING_IP (Cloudflare) but intentionally does not trust X-Forwarded-For (client-spoofable).

Context Limit
File downloads 10/min per IP
Query-string API (per action) 20/min per IP
REST API (general) 30/min per IP
REST activate/deactivate 20/min per IP
Portal billing request 3/min per IP
Lookup code send 3/min per IP
Lookup code verify 5/min per IP

Security

Bodholdt Licensing has been through a 9-phase commercial-grade security audit scoring 10/10, with 70+ fixes applied. Security measures include:

  • 128-bit license keys generated with random_bytes().
  • All API endpoints are rate-limited to prevent brute-force attacks.
  • Stripe webhook signature verification on every event.
  • CSRF protection (nonce verification) on all admin forms and AJAX handlers.
  • Prepared SQL statements on every database query.
  • XSS protection — all output escaped with esc_html(), esc_attr(), esc_url().
  • Input sanitization — all $_GET, $_POST, and $_SERVER values sanitized.
  • Email verification for license-key lookups (6-digit code with timing-safe comparison via hash_equals()).
  • Vault protection — download files are never directly accessible via URL.
  • HTTP header authentication — the auto-updater passes the license key via the X-License-Key header instead of the URL.
  • No serialized PHP data — all structured data stored as JSON.
  • Idempotent webhook processing — transient locking prevents duplicate license creation.
  • Path traversal preventionbasename() on all download filenames.
  • Settings import restricted to an allowlist of safe keys (no credential or mode injection).
  • WordPress Coding Standardswp_date(), wp_json_encode(), sanitize_callback on all registered settings.

Database tables (developer reference)

The plugin creates two custom tables per site (using dbDelta()).

{prefix}cls_licenses

Column Type Description
id mediumint AUTO_INCREMENT Primary key
license_key varchar(255) UNIQUE 128-bit hex key (32 chars)
email varchar(255) INDEXED Customer email
item_reference varchar(255) Product slug
date_created date License creation date
date_expiry date NULL Expiration date (NULL = lifetime)
status varchar(20) INDEXED active, valid, blocked, expired
max_allowed_domains tinyint DEFAULT 1 Domain activation limit
registered_domains text (JSON) Array of activated domains
txn_id varchar(255) INDEXED Stripe subscription/payment ID

{prefix}cls_activity_log

Column Type Description
id bigint AUTO_INCREMENT Primary key
license_id mediumint INDEXED Foreign key to cls_licenses
license_key varchar(255) License key at time of event
action varchar(50) INDEXED Event type
details text Event-specific details
ip_address varchar(45) Client IP (Cloudflare-aware)
created_at datetime INDEXED Event timestamp

WP-Cron events (developer reference)

Two scheduled jobs run in the background.

Hook Schedule Description
cls_daily_log_rotation Daily Deletes activity-log entries older than the configured retention (default 365 days)
cls_daily_expiry_reminders Daily Sends reminder emails at 30, 7, and 4 days before license expiry

Both events are scheduled on activation and cleared on deactivation.

What happens when you uninstall

When the plugin is deactivated, it clears transients, rate-limiter data, and cron events. All your license data and settings are preserved.

When the plugin is deleted (via Plugins → Delete), it removes:

  • All cls_* options from the options table
  • Both database tables (cls_licenses and cls_activity_log)
  • All cls_* transients
  • On Multisite: it repeats this cleanup for every site in the network

To keep your data, turn on “Preserve data on delete” in Settings → Advanced before removing the plugin.

Troubleshooting

“Store Temporarily Unavailable” on checkout

Your own Bodholdt Licensing license has expired or is missing. Go to Settings → Stripe & License and enter a valid license key. If you think this is a mistake, check that your server can reach bodholdtlabs.com (the phone-home check runs daily, with a 30-day stale threshold).

Webhook events not being received

  • Verify your webhook URL is exactly: https://yoursite.com/?cls_webhook=stripe
  • Confirm all four events are selected in Stripe.
  • Check that the Webhook Secret matches what’s in your Settings.
  • Use the Test Webhook button in Settings to confirm your URL is reachable.
  • In Stripe, go to Webhooks → your endpoint → Recent Deliveries to see error details.

Customers can’t download after purchase

  • Make sure the .zip filename in Product Manager matches the actual file in your vault directory.
  • Verify the vault directory is readable by your web-server user.
  • Check that the customer’s license status is active or valid in Manage Licenses.
  • On Nginx, make sure the deny rule blocks direct access but doesn’t interfere with PHP processing.

Auto-updater not showing updates

  • Make sure Product Versions are set and the version number is higher than what the customer has installed.
  • Customer installations cache the update check for 12 hours. They can go to Dashboard → Updates and click Check Again.
  • Verify the customer’s license key is active and the domain is registered.

License verification fails in a customer’s plugin

  • Confirm the Server URL in the SDK matches your site URL exactly (including https://).
  • Verify the Item Reference matches the product slug in your Product Manager.
  • Test the API directly: https://yoursite.com/?cls_action=cls_check&license_key=XXXX&item_reference=slug&registered_domain=example.com

Emails not being delivered

  • Install an SMTP plugin (WP Mail SMTP, FluentSMTP, etc.). The Dashboard shows a warning if none is detected.
  • Use the Send Test Email button in Settings → Emails to verify delivery.
  • Check the Activity Log for email_failed events.

Frequently asked questions

What happens to my customers if my license expires?

Nothing. Your customers’ licenses, downloads, subscriptions, and auto-updates all keep working. The only thing that stops is your ability to create new licenses and process new checkouts.

Can I use this on multiple sites?

Each Bodholdt Licensing license is valid for one domain. If you run separate licensing servers (e.g. staging and production), you’ll need a license for each.

Does this work with Multisite?

Yes. The plugin supports network-wide activation and creates its database tables for each site in the network.

Do I need WooCommerce?

No. Bodholdt Licensing handles payments directly through Stripe. No WooCommerce, Easy Digital Downloads, or any other ecommerce plugin is needed.

Can I sell themes too, or just plugins?

Both. Any WordPress software you can package as a .zip file can be sold through Bodholdt Licensing — plugins, themes, or any other extension.

Can customers have multiple licenses for the same product?

Yes. Paid products (subscriptions and one-time purchases) can be bought multiple times by the same email, so customers can license your plugin on multiple sites. Free trials are limited to one per product per email.

Support & lifetime credits

Need a hand? Open a ticket on our Support page using the email you bought with. If you’re on an annual or monthly plan, support is included while your plan is active. If you bought a lifetime license, your purchase includes support credits that never expire — 15 for a single site, 50 for 5 sites, and 150 for 25 sites — applied automatically at checkout and added to any credits you already have. One credit covers one support request; pre-sales questions, feature requests, bug reports, and billing questions are always free. The documentation home has the full support policy.