Skip to main content
Plugins Docs About Support Pricing My Account Browse Plugins

Bodholdt Licensing Docs

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

Full Guide
v10.52.8

What Bodholdt Licensing is

Bodholdt Licensing 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 you buy contains the complete plugin, and from its first activation it runs for seven days without a license key. When the seven-day 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

  1. In WordPress, open Plugins → Add New Plugin → Upload Plugin.
  2. Select bodholdt-licensing.zip, choose Install Now, and then Activate.
  3. Open Bodholdt Licensing → Getting Started. The six-step checklist links to the relevant settings and records progress for your WordPress user.
  4. 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

  1. Open Bodholdt Licensing → Settings → Payments.
  2. Paste the Stripe Secret Key for the mode you intend to use and choose Verify Connection.
  3. In Stripe, create a webhook endpoint using the exact HTTPS URL displayed in the Payments tab: https://example.com/?cls_webhook=stripe.
  4. Subscribe the endpoint to all nine required events: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed, charge.refunded, charge.dispute.created, and charge.dispute.closed. The plugin verifies the endpoint before it accepts a paid checkout, so a missing event blocks checkout until you add it.
  5. Copy that endpoint’s Signing Secret into the Payments tab and save.
  6. 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

  1. Open Bodholdt Licensing → Settings → Emails.
  2. Set the sender name, sender address, Reply-To address, and purchase/trial templates.
  3. Use Send me a test. Paid checkout stays disabled until WordPress reports a successful test-message handoff.
  4. 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

An external 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. In external mode, a relative path, a directory under wp-content/uploads, a symlink that resolves into a public directory, or an unreadable path is rejected. On a host where nothing above the web root is writable, which is normal on shared hosting, an install with no vault location set starts in a guarded vault inside uploads instead. That vault serves no file until its protection test passes or you accept secrecy-only protection, and you can change the location in Settings.

  1. Open Bodholdt Licensing → Settings → Payments and review the Storage Path status.
  2. 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.
  3. Place each release ZIP in that directory, or upload it from the Products screen.
  4. 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

  1. Create the product and its recurring or one-time Prices in Stripe.
  2. Open Bodholdt Licensing → Products and either create the Stripe product from WordPress or connect an existing Stripe product.
  3. 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.
  4. For each published release, set the current version, minimum WordPress and PHP versions, tested WordPress version, description, and changelog.
  5. 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:

  • [cls_checkout_form] for product selection and Stripe Checkout.
  • [cls_success_page] for the post-checkout confirmation.
  • [cls_portal] for email-verified license lookup, downloads, domain information, and Stripe billing management.

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.

6. Save a Stripe Customer Portal configuration

In Stripe, open Settings → Billing → Customer portal and save a configuration before you share a checkout link. The portal’s Manage billing button creates a billing-portal session, and until a configuration exists that call fails and the customer sees only a generic error.

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 nine 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 one-time purchase, without letting an older event overwrite a newer terminal state. A subscription’s license changes only when Stripe’s charge still names the subscription’s invoice; otherwise the Activity Log records the refund or dispute against the payment and the license stays as it was.

Connect a plugin or theme

  1. Finish the corresponding product record first.
  2. Open Bodholdt Licensing → Connect a Plugin, select the product, and generate the client file.
  3. Add the generated licensing.php file and bootstrap snippet to your plugin, using the generated instructions for that product.
  4. 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, and registered_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=download and the non-secret product reference. WordPress adds that product’s key in the X-License-Key request 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:

  • GET /licenses lists licenses. Requires a logged-in WordPress user with manage_options; supports page, per_page, search, and status.
  • POST /licenses creates a license. Requires manage_options; accepts product, email, optional expiry, and optional is_trial.
  • GET /license is a rate-limited license lookup. Send the key in the X-License-Key header.
  • POST /license/activate performs a rate-limited activation. Send domain plus license_key in the request body, or send the key in X-License-Key.
  • POST /license/deactivate performs a rate-limited deactivation. Send domain plus license_key in the request body, or send the key in X-License-Key.
  • GET /products is rate-limited and returns the product list.
  • GET /update-check/{slug} is rate-limited and returns 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. The link in the delivery email is signed, bound to one license and one product, and valid for 24 hours; the customer portal issues a fresh one at any time. 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 [cls_reviews product="your-product-slug"] 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 holds customer keys, product, status, tier, dates, transaction reference, and registered domains.
  • cls_activity_log holds operational activity and IP records.
  • cls_reviews holds eligible verified customer reviews.
  • cls_downloads holds authenticated download records.
  • cls_checkout_fulfillments holds 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

Start with the emailed link itself: it expires after 24 hours, and an expired link is the most common cause. The customer can request a fresh one from the customer portal at any time. If the link is current, 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.

Manage billing returns a system error

No Stripe Customer Portal configuration has been saved. Open Settings → Billing → Customer portal in Stripe, save a configuration, and try again. This is the most common cause, because nothing in the checkout flow requires the portal until a customer uses it.

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.