LucrativeIt Digital Download Licensing Pro – User Guide
Pro is an add-on for the free LucrativeIt Digital Download and Licensing plugin. It adds:
- Domain subscription packagesSell the same product as "1 Site", "5 Sites", "Unlimited Sites", each with its own price and billing period (monthly / yearly / lifetime).
- Domain activationsEvery license key is limited to the number of domains in its package. You and your customers can see and remove activated domains.
- License-gated updatesBuyers update your WordPress plugin from their dashboard, or your Laravel / PHP script with one command, using their license key.
1. Installation
- Install and activate LucrativeIt Digital Download and Licensing (free).
- Upload the
lucrativeit-digital-download-licensing-profolder towp-content/plugins/and activate it. - A Pro section appears in the LucrativeIt Digital Download sidebar, under the normal menu:
- Pro Settings
- User Guide (this page)
- The product editor gets two Pro panels: Price per number of domains in step 2 (Pricing) and Automatic updates with license key in step 4 (Product file).
- The normal Licenses screen gets a Domains column with a Manage button for each license.
2. Seller setup (your store)
2.1 Prepare the product
Go to LucrativeIt Digital Download → Products and edit (or add) your product:
| Field | Example | Used for |
|---|---|---|
| Product type | WordPress plugin / Laravel script | Shown in the API |
| Software version | 1.0.0 | Clients compare it with their installed version |
| Changelog | = 1.0.1 = Fixed… | Shown in the update popup / artisan output |
| File | my-plugin.zip | The file delivered as the update |
1.0.0 → 1.0.1). That's it.2.2 Price per number of domains
Open the product editor: Products → Edit → step 2 (Pricing). Below Plan prices you'll find the Price per number of domains panel with one list per plan: Lifetime, Yearly and Monthly. A list is dimmed while its plan is switched off above.
- + Add domains adds a row under that plan. Type the number of domains and the price.
0domains = unlimited. - Add the plan price as the first row (for example Yearly price
54→ row1domain /54). Buyers who keep the first choice then get that domain limit. - The trash icon removes a row. Licenses already sold keep their domain limit.
- Click Save & Continue: the prices are saved together with the pricing step. Each number of domains needs its own price within a plan.
Example for a product with Yearly price 54 and Lifetime price 199:
| Plan | Domains | Price |
|---|---|---|
| Yearly | 1 | 54 |
| Yearly | 2 | 89 |
| Yearly | 5 | 149 |
| Lifetime | 1 | 199 |
| Lifetime | 0 (unlimited) | 499 |
Each row is named after its domains (2 sites, Unlimited sites) and includes automatic updates.
2.3 What buyers see
Nothing to paste: every price card of the product automatically gets a Number of websites choice under its features. Choosing an option updates the price on the card, and Checkout opens your normal checkout page with that price. After payment the license key is emailed as usual and the domain limit is attached to it automatically.
While a larger option is chosen, Add to cart is hidden on that card (the cart sells the plain plan price). A plan with only one row shows its domain limit as a line (for example "1 site") instead of a choice.
The [lddl_pro_packages product_id="123"] shortcode still works if you want a separate package table on another page.
2.4 Customer domain management
Put this shortcode on the customer account page (or any page for logged-in customers):
[lddl_pro_my_licenses]
Customers see each license, the package, domains used / allowed, and can Deactivate a domain to move the license to another site (can be disabled in Pro Settings).
2.5 Enable updates for a product
Open the product editor: Products → Edit → step 4 (Product file). Below Current version and Changelog you'll find the Automatic updates with license key panel:
- Upload the ZIP and set Current version (and Changelog).
- Switch on Enable license-gated updates for this product.
- Update slug – for WordPress plugins this must be the plugin folder name (e.g.
my-awesome-plugin). For Laravel scripts use any unique slug (e.g.my-laravel-app). - Optional: Requires WordPress, Tested up to, Requires PHP, icon and banner (shown in the WordPress "View details" popup).
- Click Publish / Update product. The product and its update settings are saved together.
- Note the product_id shown under Client config at the bottom of the panel – you put it in the client SDK.
2.6 Pro settings
| Setting | Default | Meaning |
|---|---|---|
| Default domain limit | 1 | Used when a license has no package and the product has no default package |
| Development domains are free | On | localhost, 127.0.0.1, *.test, *.local, staging.*, dev.* don't count |
| Only deliver updates to activated domains | On | Update downloads require an activation for the requesting domain |
| Customers can deactivate their domains | On | Shows the Deactivate button in [lddl_pro_my_licenses] |
| Download link lifetime | 60 min | Signed update links expire after this time |
| API requests per IP per minute | 60 | Basic brute-force protection |
2.7 Manage a license (admin)
Open Licenses. The Domains column shows domains used / allowed for every license (red when the limit is reached). Click Manage to open the license below its row:
- Purchased option: switch the license to another number of domains of the product (upgrade/downgrade a customer).
- Set a custom domain limit for one license (overrides the purchased option).
- See every activated domain, platform, installed version and last check-in.
- Deactivate a domain or activate one manually.
3. How the buyer uses the license key
- Buys a package → receives
LUCRATIVEDIGIPROD-XXXX-XXXX-XXXX-XXXXby email / on the account page. - Enters the key in your product:
- WordPress plugin: Settings → My Plugin License → Activate license.
- Laravel script:
php artisan license:activate LUCRATIVEDIGIPROD-XXXX-XXXX-XXXX-XXXX.
- The site's domain is activated (counts toward the package limit).
- Updates:
- WordPress: appear in Dashboard → Updates and Plugins, installed with Update now (auto-updates work too).
- Laravel:
php artisan license:update.
- When the subscription expires, the site still sees that a new version exists, but the download is blocked with "renew your license".
- To move to a new site: deactivate on the old site (or from the store account page), then activate on the new one.
4. WordPress plugin integration (for the seller)
Copy sdk/wordpress/class-lddl-license-updater.php into the plugin you sell, e.g. my-awesome-plugin/includes/.
<?php
/**
* Plugin Name: My Awesome Plugin
* Version: 1.0.0
*/
define( 'MY_AWESOME_PLUGIN_VERSION', '1.0.0' );
require_once __DIR__ . '/includes/class-lddl-license-updater.php';
function my_awesome_plugin_license() {
static $updater = null;
if ( null === $updater ) {
$updater = new LDDL_License_Updater( array(
'store_url' => 'https://your-store.com/',
'product_id' => 123,
'slug' => 'my-awesome-plugin', // = plugin folder name = Update slug
'plugin_file' => __FILE__,
'version' => MY_AWESOME_PLUGIN_VERSION,
'name' => 'My Awesome Plugin',
) );
}
return $updater;
}
add_action( 'plugins_loaded', 'my_awesome_plugin_license' );
// Optional: premium features only with an active license.
add_action( 'init', function () {
if ( my_awesome_plugin_license()->is_active() ) {
// register premium features…
}
} );
A complete example is in sdk/wordpress/example-plugin/my-awesome-plugin/.
ZIP structure (upload this as the product file):
my-awesome-plugin.zip
└── my-awesome-plugin/
├── my-awesome-plugin.php
└── includes/class-lddl-license-updater.php
What the updater does:
- Adds Settings → My Awesome Plugin License (key field, Activate / Deactivate / Check for updates).
- Hooks into WordPress' update system (
update_pluginstransient) and the "View details" popup. - Fetches a fresh signed download link right before installing (links are short-lived).
is_active()lets you lock premium features.- Optional config:
menu_parent(put the license page under your own menu),cache_hours(default 6),capability,timeout(seconds to wait for your store, default 15).
5. Laravel script integration (for the seller)
5.1 Add the package to your script
Copy sdk/laravel into your project, e.g. packages/lddl-license, and add to your script's composer.json:
"repositories": [
{ "type": "path", "url": "packages/lddl-license" }
],
"require": {
"lucrativeit/lddl-license": "*"
}
Run composer update lucrativeit/lddl-license. The service provider is auto-discovered.
(Optional) publish the config: php artisan vendor:publish --tag=lddl-license.
5.2 .env of the buyer's installation
LDDL_STORE_URL=https://your-store.com/
LDDL_PRODUCT_ID=123
LDDL_APP_VERSION=1.0.0
LDDL_LICENSE_KEY=
Ship LDDL_STORE_URL, LDDL_PRODUCT_ID and LDDL_APP_VERSION in your .env.example; the buyer only adds the key (or uses license:activate).
5.3 Artisan commands
php artisan license:activate LUCRATIVEDIGIPROD-XXXX-XXXX-XXXX-XXXX # activate this domain
php artisan license:status # validity, package, domains, expiry
php artisan license:update --check # is there a new version?
php artisan license:update # download + install
php artisan license:update --yes # no confirmation (cron / CI)
php artisan license:deactivate # free this domain
license:update does:
- Checks the store (license valid + domain activated + package includes updates).
- Downloads the ZIP through a signed link.
- Backs up the app to
storage/app/lddl-backups/(excludingvendor,node_modules,storage,.git). php artisan down→ extracts the ZIP over the app (a single top-level folder in the ZIP is stripped) without touching.env,storage,bootstrap/cache,public/storage,public/uploads.- Runs
migrate --forceandoptimize:clear(configurable inafter_update). php artisan upand saves the new installed version.
If your ZIP does not contain vendor/, add composer install --no-dev to your own post-update instructions.
5.4 Protect routes with the license
// routes/web.php
Route::middleware(['web', 'license'])->group(function () {
Route::get('/dashboard', DashboardController::class);
});
The check is cached (cache_minutes, default 12 h) and keeps working for grace_days (default 7) if your store is unreachable. Set redirect_route in the config to redirect to an "Enter license" page instead of a 403.
5.5 Use the manager in code
use Lddl\License\LicenseManager;
$license = app(LicenseManager::class);
if (! $license->isActive()) {
// show "activate your license" banner
}
$update = $license->checkUpdate();
if ($update['success'] && version_compare($update['new_version'], $license->installedVersion(), '>')) {
// show "Version {$update['new_version']} is available" in your admin panel
}
5.6 In-app "Update now" button (optional)
use Lddl\License\LicenseManager;
use Lddl\License\Updater;
Route::post('/admin/update', function (LicenseManager $license, Updater $updater) {
$update = $license->checkUpdate();
$log = [];
$version = $updater->run($update, function ($line) use (&$log) { $log[] = $line; });
return back()->with('status', "Updated to {$version}")->with('log', $log);
})->middleware(['web', 'auth', 'can:admin']);
5.7 Daily license check (optional)
// app/Console/Kernel.php (Laravel ≤10) or routes/console.php (Laravel 11+)
Schedule::command('license:status')->daily();
6. Plain PHP scripts (no framework)
Use sdk/php/LddlLicenseClient.php (needs cURL + Zip):
require __DIR__ . '/LddlLicenseClient.php';
$client = new LddlLicenseClient([
'store_url' => 'https://your-store.com/',
'product_id' => 123,
'version' => '1.0.0',
'domain' => $_SERVER['HTTP_HOST'],
'platform' => 'php',
]);
$key = 'LUCRATIVEDIGIPROD-XXXX-XXXX-XXXX-XXXX';
$res = $client->activate($key);
if (! $res['success']) {
exit($res['message']);
}
$update = $client->checkUpdate($key);
if ($update['success'] && $update['update_available'] && $update['download_url']) {
$zip = $client->download($update['download_url'], __DIR__ . '/tmp');
$client->install($zip, __DIR__, ['.env', 'config.php', 'uploads']);
// save $update['new_version'] as your installed version
}
7. REST API reference
Base URL: https://your-store.com/wp-json/lddl-pro/v1
All responses are JSON with success, code and message. Send parameters as form fields or JSON.
| Endpoint | Params | Success response |
|---|---|---|
POST /license/activate | license_key, domain, platform, version, optional product_id / slug | domain, activated_at, license{…} |
POST /license/deactivate | license_key, domain | domain, license{…} |
GET/POST /license/check | license_key, domain, version | activated: true, license{…} |
GET/POST /update/check | license_key, domain, version, optional product_id / slug | update_available, new_version, download_url (or null + download_blocked), changelog, requires_wp, tested_wp, requires_php, license{…} |
GET /update/info | slug or product_id | Name, version, description, changelog, icons, banners |
GET /update/download | Signed URL from /update/check | The ZIP file |
license{…} object:
{
"status": "active",
"product_id": 123,
"product_name": "My Awesome Plugin",
"plan": "yearly",
"expires_at": "2027-09-30T10:00:00+00:00",
"package": "Business – 5 Sites",
"max_domains": 5,
"domains_used": 2,
"domains_left": 3,
"updates_enabled": true
}
Error codes:
| Code | HTTP | Meaning |
|---|---|---|
invalid_license | 403 | Key not found |
license_expired | 403 | Subscription expired / cancelled |
domain_limit_reached | 409 | All package slots are used |
not_activated | 403 | Domain not activated for this key |
product_mismatch | 403 | Key belongs to another product |
updates_disabled | 403 | Package has no updates |
no_release | 404 | Updates not enabled / no version / no file |
invalid_domain | 400 | Missing domain |
rate_limited | 429 | Too many requests |
cURL examples:
curl -X POST https://your-store.com/wp-json/lddl-pro/v1/license/activate \
-d license_key=LUCRATIVEDIGIPROD-XXXX-XXXX-XXXX-XXXX -d domain=client.com -d platform=laravel -d version=1.0.0
curl -X POST https://your-store.com/wp-json/lddl-pro/v1/update/check \
-d license_key=LUCRATIVEDIGIPROD-XXXX-XXXX-XXXX-XXXX -d domain=client.com -d version=1.0.0
Domains are normalised: https://www.Client.com/shop → client.com.
8. Developer hooks (store side)
| Hook | Type | Arguments |
|---|---|---|
lddl_pro_domain_activated | action | $license, $domain |
lddl_pro_domain_deactivated | action | $license_id, $domain |
lddl_pro_license_package_attached | action | $license, $package |
lddl_pro_update_downloaded | action | $license, $domain, $release |
lddl_pro_is_local_domain | filter | $is_local, $host |
lddl_pro_client_ip | filter | $ip (e.g. read CF-Connecting-IP behind Cloudflare) |
9. Troubleshooting
| Problem | Fix |
|---|---|
| WordPress says "Automatic update is unavailable" | License expired, domain not activated, package without updates, or no file uploaded. Check Licenses → Manage. |
| WordPress installs into a wrong folder | The ZIP must contain the plugin folder (named like the slug) at its root. |
| No update shown | Raise Software version on the product; on the client click Dashboard → Updates → Check again or the license page's Check for updates now. |
domain_limit_reached | Deactivate an old domain (customer account page or admin), upgrade the package, or set a custom limit. |
rate_limited behind a proxy/CDN | Use the lddl_pro_client_ip filter to read the real client IP. |
| Package price not used at checkout | Choose the number of websites on the price card and use Checkout (it adds lddl_package to the checkout URL). The cart sells the plain plan price. |