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.37.6

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

  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 seven required events: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed, charge.refunded, charge.dispute.created, and charge.dispute.closed.
  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

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.

  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:

  • Total: $0.00
    Secure payment powered by
    for product selection and Stripe Checkout.
  • 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 the post-checkout confirmation.
  • Manage Subscription

    Enter the email used during checkout.

    Billing & Subscriptions

    Cancel, upgrade, update payment method, or view invoices via Stripe.

    A secure access link will be emailed to you.

    License Keys

    Look up your active license keys and product details.

    A verification code will be emailed to you for security.

    Support & Tickets

    View your support tickets, check your support status, and manage support credits.

    Open Support & Tickets

    Sign in with a secure link sent to your email.

    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.

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

  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 in 10.37.3:

  • GET /licenses — list licenses. Requires a logged-in WordPress user with manage_options; supports page, per_page, search, and status.
  • POST /licenses — create a license. Requires manage_options; accepts product, email, optional expiry, and optional is_trial.
  • GET /license — rate-limited license lookup; send the key in the X-License-Key header.
  • POST /license/activate — rate-limited activation; send domain plus license_key in the request body, or send the key in X-License-Key.
  • POST /license/deactivate — rate-limited deactivation; send domain plus license_key in the request body, or send the key in X-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.