Bodholdt Licensing Docs
Set up your self-hosted licensing system from scratch. Full integration guide included.
What Bodholdt Licensing is
Bodholdt Licensing 10.37.3 is a self-hosted licensing and commerce system for WordPress plugin and theme sellers. It runs on your WordPress site, uses Stripe Checkout for payment, issues customer license keys, limits domain activations, serves authenticated updates from a private ZIP vault, and provides a customer portal, reports, activity records, verified reviews, and a generated client SDK.
The plugin requires WordPress 6.0 or newer, PHP 8.0 or newer, and HTTPS in production. Stripe credentials are required only when you want to take payments. Reliable WordPress email delivery is required before paid checkout can be enabled.
Edition, evaluation, and plan limits
There is no separate free or trial download of Bodholdt Licensing. The commercial ZIP contains the complete plugin and gives a fresh installation a built-in 7-day evaluation without a license key or credit card. When that evaluation ends, enter a valid Bodholdt Licensing key to continue issuing new customer licenses and taking checkout orders. Existing customer records are retained, and their validation, domain activation/deactivation, portal, and authenticated-download paths continue to operate while you resolve the store’s commercial key.
The plan entitlement applies to the sites where you run Bodholdt Licensing:
- Hobby: 1 activated site and 1 product.
- Studio: up to 5 activated sites and unlimited products.
- Foundry: up to 127 activated sites and unlimited products.
Revenue and license-health Reports are a Studio-or-higher feature. Hobby remains a complete single-product licensing store, including checkout, customer keys, authenticated updates, the portal, activity records, reviews, and the generated client.
Each activated hostname counts, including staging and development hostnames. The number of sites allowed on a customer license is a separate product setting that you control. Every Bodholdt Licensing plan is also covered by the published 14-day money-back guarantee; that guarantee is separate from the seven-day plugin evaluation and from any customer-product trials you configure in your own store.
Install and activate
- In WordPress, open Plugins → Add New Plugin → Upload Plugin.
- Select
bodholdt-licensing.zip, choose Install Now, and then Activate. - Open Bodholdt Licensing → Getting Started. The six-step checklist links to the relevant settings and records progress for your WordPress user.
- Open Bodholdt Licensing → Settings → License and enter the key from your purchase when you are ready to license the installation. A candidate key is verified and the current site is activated before it replaces a working key.
On WordPress Multisite, only a network administrator can view or change the network-wide Bodholdt Licensing key. A network activation uses the network’s canonical hostname for this commercial activation.
Current interface




Prepare a production store
Complete these items before sharing a checkout link. Bodholdt Licensing checks the critical fulfillment dependencies and fails closed instead of accepting a paid order it cannot safely deliver.
1. Connect Stripe
- Open Bodholdt Licensing → Settings → Payments.
- Paste the Stripe Secret Key for the mode you intend to use and choose Verify Connection.
- In Stripe, create a webhook endpoint using the exact HTTPS URL displayed in the Payments tab:
https://example.com/?cls_webhook=stripe. - Subscribe the endpoint to all seven required events:
checkout.session.completed,customer.subscription.updated,customer.subscription.deleted,invoice.payment_failed,charge.refunded,charge.dispute.created, andcharge.dispute.closed. - Copy that endpoint’s Signing Secret into the Payments tab and save.
- Send a test delivery from that Stripe webhook endpoint. Checkout remains closed until Bodholdt Licensing has verified the enabled endpoint, exact site URL, live/test mode, required event set, and a real Stripe-signed delivery using the currently saved Signing Secret.
Use Stripe test-mode values and test prices together while validating the store, then replace all of them together for live mode. Do not publish API keys, webhook secrets, license keys, or unredacted screenshots.
2. Verify email delivery
- Open Bodholdt Licensing → Settings → Emails.
- Set the sender name, sender address, Reply-To address, and purchase/trial templates.
- Use Send me a test. Paid checkout stays disabled until WordPress reports a successful test-message handoff.
- Confirm the test actually arrives. A successful
wp_mail()result means WordPress handed the message to its mail transport; it does not by itself prove inbox delivery. Configure SMTP or a transactional-mail service when your host’s default mail is unreliable.
3. Create a private product vault
The ZIP vault must be an absolute server path outside WordPress, uploads, and every known public document root. A path such as /srv/private-product-vault is suitable when the PHP process can access it and the web server cannot serve it directly. A relative path, a directory under wp-content/uploads, a symlink that resolves into a public directory, or an unreadable path is rejected. There is no public-uploads fallback.
- Open Bodholdt Licensing → Settings → Payments and review the Storage Path status.
- Create the external directory with least-privilege ownership and permissions. PHP needs read access for downloads and write access if you upload ZIPs through WordPress.
- Place each release ZIP in that directory, or upload it from the Products screen.
- Keep backups outside the web root. Bodholdt Licensing deliberately leaves this external vault in place when the plugin is uninstalled.
The Products upload interface accepts ZIPs up to 100 MB, but the effective limit may be lower because of PHP, proxy, or web-server upload settings.
4. Configure products
- Create the product and its recurring or one-time Prices in Stripe.
- Open Bodholdt Licensing → Products and either create the Stripe product from WordPress or connect an existing Stripe product.
- Set a stable product slug, customer-facing name, exact ZIP filename, Stripe Price IDs, sites allowed per customer license, and optional customer trial days. A trial value of zero disables that product’s trial.
- For each published release, set the current version, minimum WordPress and PHP versions, tested WordPress version, description, and changelog.
- Save and confirm the row reports that it is ready to sell. Hobby is limited to one unique product across a Multisite network; Studio and Foundry allow unlimited products.
A bundle issues licenses for its component products. Keep product slugs stable after customers have installed generated clients, because the slug links the license, update metadata, and archive together.
5. Generate and review store pages
Use Generate Store Pages on the Products screen. The plugin creates standard WordPress pages containing:
for product selection and Stripe Checkout.for the post-checkout confirmation.Purchase details unavailable
This private page shows an order only when opened from a current checkout or delivery email link.
No private order link was provided.
Use Manage Licenses to securely look up your keys, or return to the store to begin a purchase.
for email-verified license lookup, downloads, domain information, and Stripe billing management.Manage Subscription
Enter the email used during checkout.
Enter the 6-digit code sent to your email:
Didn't receive it? Check your spam folder, or click Resend Code. Wrong email?
Preview all three pages while signed out and on a phone-sized viewport. You may edit their titles or surrounding content, but retain the shortcode and keep the success URL configured as the Stripe Checkout return URL. An existing published page can be adopted when it already contains the matching shortcode; an occupied slug without that shortcode is rejected instead of being reported as ready.
Paid-checkout readiness gate
For a paid product, checkout is allowed only when all of the following are ready:
- The request is HTTPS.
- The Stripe Secret Key is valid; the enabled Stripe webhook matches this site and mode, includes all seven required events, and has delivered a valid signature using the currently saved Signing Secret.
- The license, activity, and checkout-fulfillment tables have the expected transactional schema.
- The private vault is safe and readable.
- Every product in the paid checkout has a matching readable ZIP file.
- Sender and Reply-To addresses are valid and a test email has succeeded through WordPress.
- The Bodholdt Licensing commercial evaluation or license is active.
If one check fails, the visitor receives an unavailable message before Stripe Checkout is created. The administrator should correct the corresponding Dashboard or Settings status rather than bypassing the check.
How fulfillment stays consistent
Stripe’s signed checkout.session.completed webhook is the authority for a paid order. Bodholdt Licensing serializes fulfillment for that order, creates the exact licenses and a pending fulfillment record in one database transaction, and then sends the purchase email. If email handoff fails, the webhook returns a retryable failure so Stripe can deliver the same event again. The existing fulfillment record is reused, so a retry does not mint duplicate licenses.
Only a fulfillment marked delivered authorizes the success page. That page loads the exact license IDs recorded for the Stripe session; it does not search recent licenses by an email address. Authorization is short-lived, and full license keys should still be treated as credentials.
Subscription update and deletion webhooks synchronize customer license status and expiration. Cancellation at period end keeps the license active until Stripe’s current billing period ends; an immediate deletion expires it immediately. Refund and dispute events apply the configured payment-reversal policy across every license in the affected purchase, including subscription invoices, without letting an older event overwrite a newer terminal state.
Connect a plugin or theme
- Finish the corresponding product record first.
- Open Bodholdt Licensing → Connect a Plugin, select the product, and generate the client file.
- Add the generated
licensing.phpfile and bootstrap snippet to your plugin, using the generated instructions for that product. - Test with a synthetic customer license on a non-production client site: save the key, activate the current domain, verify the status, deactivate it, and test a WordPress update.
The generated client stores the verified tier for Hobby, Studio, and Foundry feature gates and treats the current site as licensed only when the server confirms both a valid status and that the domain is registered.
Credential transport used by generated clients
- License check, activation, and deactivation use an HTTPS POST body containing
cls_action,license_key,item_reference, andregistered_domain. The key is not placed in the URL. - Update metadata uses an HTTPS GET with the product reference and no license key.
- The archive URL contains only
cls_action=downloadand the non-secret product reference. WordPress adds that product’s key in theX-License-Keyrequest header only when both the exact configured HTTPS server origin and product reference match. - Credential-bearing generated-client requests do not follow redirects, require certificate verification, and fail closed when the configured server is not HTTPS.
The license server rejects insecure public license, download, checkout, and REST traffic with HTTP 426. Terminate TLS correctly at the origin or configure WordPress’s standard proxy HTTPS detection so the application sees the request as secure.
REST API reference
The REST namespace is /wp-json/bodholdt-licensing/v1/. These are the complete routes in 10.37.3:
GET /licenses— list licenses. Requires a logged-in WordPress user withmanage_options; supportspage,per_page,search, andstatus.POST /licenses— create a license. Requiresmanage_options; acceptsproduct,email, optionalexpiry, and optionalis_trial.GET /license— rate-limited license lookup; send the key in theX-License-Keyheader.POST /license/activate— rate-limited activation; senddomainpluslicense_keyin the request body, or send the key inX-License-Key.POST /license/deactivate— rate-limited deactivation; senddomainpluslicense_keyin the request body, or send the key inX-License-Key.GET /products— rate-limited product list.GET /update-check/{slug}— rate-limited update metadata for one product.
There are no REST update or delete routes for licenses. Use normal WordPress REST authentication for the two administrative routes. License keys are never accepted in REST URL paths or query strings. Treat each key as a credential and do not paste a real key into screenshots, support tickets, or analytics.
Customer portal, downloads, and updates
The portal verifies access with a six-digit email code before returning customer licenses. Customers can see their licensed products and registered domains, download eligible archives, and open Stripe’s hosted billing portal when a Stripe customer record is available.
Paid archives are streamed from the external vault only after license and product checks pass. Downloads are rate-limited and recorded for reports. Generated clients integrate update metadata with WordPress’s native plugin-update UI; normal update timing is controlled by WordPress and cached metadata, so use WordPress’s Check again control when validating a just-published release.
Reviews, reports, activity, and privacy
Use to render verified reviews for one product. Optional attributes include heading, limit, schema="no", and empty="yes". Aggregate-rating schema is emitted only when eligible real reviews exist.
The Activity Log records operational events and IP addresses. Retention can be set from 30 to 365 days and is enforced by a daily task. The per-customer privacy export includes relevant license, activity, and fulfillment data. The same task removes stored fulfillment delivery-error details after 30 days by default. Developers can change that window with the cls_fulfillment_error_retention_days filter; the minimum is two days. Fulfillment email, status, Stripe session/transaction references, and the exact license ledger remain because they enforce delivery identity and keep delayed webhooks idempotent. Reports summarize license, payment, subscription, review, and download activity; Stripe report data is cached to avoid unnecessary API traffic.
Bodholdt Licensing stores the email addresses, license keys, activated domains, fulfillment state, and operational records needed to run your store. Stripe collects card details on its hosted pages; card details do not pass through the plugin. License/subscription records are not automatically erased because doing so can revoke service and destroy transaction records; evaluate and process verified erasure requests under your legal and accounting obligations. These controls support your privacy process, but you remain responsible for notices, lawful basis, retention choices, and data-subject requests.
Database and scheduled work
Each site stores operational data in five prefixed tables:
cls_licenses— customer keys, product, status, tier, dates, transaction reference, and registered domains.cls_activity_log— operational activity and IP records.cls_reviews— eligible verified customer reviews.cls_downloads— authenticated download records.cls_checkout_fulfillments— durable, exact-order Stripe fulfillment and email-attempt state.
Daily scheduled work covers activity-log rotation, license-expiry reminders, eligible review requests, and verification of this installation’s Bodholdt Licensing key. WordPress cron runs on site traffic by default; for a low-traffic production store, call wp-cron.php from a real system scheduler.
Multisite behavior
Network activation creates the five tables for existing subsites and initializes storage for subsites added later. Products, customer licenses, fulfillment records, activity, reviews, downloads, and most operational options belong to the individual site where the store runs.
The Bodholdt Licensing key, verified tier, evaluation clock, and plan entitlement are network-wide. Only network administrators can change that key. Hobby’s one-product entitlement is enforced network-wide by unique product identifier, even though the product records themselves are per site.
Deactivation and uninstall
- Deactivation clears Licensing’s scheduled jobs and short-lived rate-limit, portal, and checkout-access state. It preserves settings, products, customer licenses, tables, fulfillment history, and the external ZIP vault.
- Uninstall with Keep your data enabled leaves plugin data in place for a later reinstall.
- Normal uninstall removes the five tables on each site, Licensing options and transients, scheduled jobs, network-wide Licensing key state, and only Licensing’s rows from shared Bodholdt Arcade tables. It attempts to release this site’s Bodholdt Licensing activation. Database removal is irreversible.
- The external product vault is never deleted automatically. Remove those archives manually only after you have retained any required backups and fulfilled your customer obligations.
Troubleshooting
Checkout says it is unavailable
Review the Dashboard checklist, then verify HTTPS, Stripe credentials and webhook secret, email test status, database tables, the external vault, each selected product ZIP, and this installation’s commercial license. The gate intentionally stops before creating a Stripe Checkout Session.
A webhook keeps retrying
Check the Activity Log and your WordPress mail transport. A failed purchase-email handoff returns a retryable response; once email succeeds, the existing exact-order fulfillment is marked delivered without creating duplicate keys.
A client is valid but a feature remains locked
Confirm the current hostname appears in the license’s registered domains and that the generated client receives domain_registered: true. Also confirm the server reports the expected tier_key.
A product update does not appear immediately
Confirm the Product Versions record has a higher semantic version and the exact ZIP filename exists in the vault. Then use WordPress’s update Check again control on the client site.
A download returns an error
Confirm the vault status, filename, PHP read permissions, active license, matching product, and registered-domain/site limits. Do not move a paid archive into public uploads as a workaround.
Getting help
For account, refund, or technical help, open a ticket on the Support page. Include the plugin version, WordPress and PHP versions, the exact error text, and relevant Activity Log timestamps. Mask all license keys, Stripe values, customer emails, domains, IP addresses, and filesystem usernames or paths before attaching logs or screenshots.