A marketplace-based license key management system for Larapen with API verification, domain/machine activation tracking, abuse blocking, and webhook integration.

Multiple Marketplaces

Keys sold on the in-site Shop, Envato Market and Gumroad, keys issued by hand, and keys pushed by external webhooks: all in one system.

Activation Tracking

Track activations per domain, machine ID, or IP. Enforce per-key limits (e.g. 3 domains max).

Customer Portal

Customers register a purchase code, see the licenses they own, and free a domain they no longer use: on your own site.

REST API

Verify, activate, and deactivate endpoints for client integration.

Blocklist New

Ban by IP, domain, or key, with optional .htaccess rules that block abuse at the Apache layer before PHP is even loaded. Auto-ban on rate-limit hits.

API Logs & Cache New

Full audit trail of every API call with redacted payloads. Centralized cache layer keeps verifications free of DB queries on cache hits.

Use Cases

SaaS Application

Your SaaS product uses self-hosted license keys to control plan tiers.

  • Each plan maps to a Product, and the plan level is carried by the key’s license type: standard (Starter), extended (Pro).
  • The application calls GET /api/licenses/verify at boot and reads product and license_type to unlock the right tier.
  • No domain activation needed: just verification.

Desktop Application

A desktop app is licensed per machine.

  • On launch, the app calls POST /api/licenses/activate with a machine_id.
  • The app periodically calls GET /api/licenses/verify to check validity.
  • When the user decommissions a machine, call POST /api/licenses/deactivate to free the slot.

WordPress Plugin / Theme

A premium WordPress plugin validates its license on the customer's site.

  • Admin enters the key in WP settings. The plugin calls POST /api/licenses/activate with domain = site_url().
  • Verification runs daily via WP-Cron using GET /api/licenses/verify.
  • If activation limit is reached, the customer must deactivate an old domain before activating a new one.

API Access Tiers

You sell API access with different rate limits per plan.

  • One Product per plan, Basic and Enterprise, or one Product whose keys carry a standard or extended license type.
  • Your API gateway calls GET /api/licenses/verify, reads product and license_type, and maps them to the rate limit it enforces.

Envato (CodeCanyon) Products

You sell a product on CodeCanyon and want automatic license verification.

  • Enable the Envato marketplace and enter your API token and author username.
  • Customers enter their Envato purchase code. The system verifies it directly against the Envato API.
  • No manual key creation needed: the Envato marketplace creates the key the first time it is verified.

JavaScript Library / Plugin

You sell a premium JS editor component. Each customer receives a license key valid for N domains.

  • On page load, the library calls POST /api/licenses/activate with the key and current domain.
  • The server verifies the key, activates on the domain (counting against the limit), and returns the license type, product and expiry via GET /api/licenses/verify, so the library enables the tier the customer paid for (standard, extended, …).

Requirements

  • Larapen CMS v1.0.0 or later
  • PHP 8.3+
  • MySQL 8.0+
  • An active Larapen installation with the add-on system enabled

Installation

Step 1: Upload the Add-on

In the admin panel, go to Admin → Extensions → Add-ons and click the Upload Add-on button. Select the add-on’s ZIP file: the system extracts it automatically and the add-on appears in the installed add-ons list.

Step 2: Activate the Add-on

Find License Management in the list and click Activate. Its migrations, seeders (if any) and permissions are set up automatically.

Step 3: Configure

Navigate to Admin → Licenses → Settings to set your API key, key format preferences, and the marketplaces you sell on (see Marketplaces). See Configuration.

Purchase Code (License Key)

License Management is sold as a separate product, so it has its own purchase code (license key), distinct from the purchase code of the main application and from the one of every other add-on. You are asked for it when you activate License Management in Admin panel → Add-ons.

Our products are sold on three platforms. The way you receive a purchase code depends on where you bought the product.

Platform / Marketplace How you get the purchase code Where to find it again
bedigit.com Store
In-site purchase (Shop)
Generated automatically when the order is paid, then sent by email, either in its own license email, or inside the order confirmation email. My Account → My Licenses on bedigit.com
Gumroad Created as soon as Gumroad notifies us of the sale, then sent in a separate email, in addition to the Gumroad receipt. The license email, your Gumroad Library, and My Account → My Licenses on bedigit.com
Envato Market
CodeCanyon
Issued by Envato, not by us, and never sent by email: you download it yourself from your Envato account. Envato account → Downloads → License certificate & purchase code
Check your spam folder. For both bedigit.com Store and Gumroad purchases, the purchase code is delivered by email. Automated license emails are very often filtered, so if the message is not in your inbox, look in your spam / junk folder before contacting support, and add our sender address to your contacts or allow list.

1. bedigit.com Store (in-site purchase)

  • As soon as the order’s payment status becomes Paid, a license key is generated automatically for every licensed item in the order (one key per purchased unit: buying 3 units gives 3 distinct keys).
  • It is emailed to the address used on the order, either in a dedicated license email or inside the order confirmation email. Check your inbox and your spam / junk folder.
  • The key stays available in your account under My Account → My Licenses. Keys are masked in the list; open the license detail page to reveal and copy the full key, see the domains it is activated on, and deactivate a domain to free an activation slot.
  • The matching invoice is under My Account → My Orders.

2. Gumroad

  • A Gumroad purchase produces two separate emails: the Gumroad receipt (sent by Gumroad, giving access to the files) and a license key email (sent by bedigit.com) that contains your purchase code.
  • The license key email is generated as soon as Gumroad notifies us of the sale, so it normally arrives within seconds of the payment. Here too, check your inbox and your spam / junk folder.
  • When the Gumroad product uses Gumroad’s own license-key feature, the same key also appears in your Gumroad receipt and under Library → your purchase on gumroad.com.
  • Use the same email address on bedigit.com as on Gumroad: your keys are then linked to your account automatically and listed under My Account → My Licenses, even if you register after the purchase. You can also add a Gumroad key manually from My Account → My Gumroad Licenses.

3. Envato Market (CodeCanyon)

  • Envato purchase codes are issued and delivered by Envato Market, never emailed by us, so there is nothing to look for in your spam folder: you retrieve the code from your Envato account.
  • Log in to your Envato / CodeCanyon account, open the Downloads page, find the item, and choose License certificate & purchase code from the Download dropdown. The code is written in that certificate.
  • An Envato purchase code looks like 12345678-90ab-cdef-1234-567890abcdef (8-4-4-4-12 characters). It never changes, and renewing item support does not issue a new one.
  • Official Envato article: Where Is My Purchase Code?
Lost your purchase code? Search your mailbox (spam folder included) for “license” or “purchase code”, then check My Account → My Licenses on bedigit.com for Store and Gumroad purchases, or Downloads → License certificate on Envato. If it is still missing, open a ticket on our Help Center with your order number (Store), Gumroad sale ID or buyer email (Gumroad), or Envato username and item name (Envato).

Configuration

All settings can be configured in two ways:

  1. Admin panel: Licenses → Settings (stored in the settings table, group licenses)
  2. Environment variables: in .env (used as defaults; admin settings override them)
Setting Description Default
api_key API key required for all client-facing endpoints. Sent via X-Api-Key header or api_key query parameter. (empty)
default_key_format Format preselected on the key forms: uuid, alphanumeric, prefixed, or hmac_signed (see Key Formats & Prefix). alphanumeric
key_prefix The leading word of the prefixed format (e.g. LIC in LIC-A1B2C-D3E4F-G5H6J-K7L8M). Letters only, max 10, uppercased when the key is built. Unused by the other three formats, so the field is only shown while default_key_format is prefixed. LIC
default_max_activations Default activation limit when creating new keys. 1
hmac_key Secret key for HMAC-signed key format and webhook signature verification. (empty, falls back to APP_KEY)
api_rate_limit Maximum API requests per minute per client. 3
api_daily_rate_limit Maximum API requests per day per client. 50

Legacy Verification Endpoint Editable in v1.0.15

If you are migrating from an existing license verification system, your already-published software keeps calling the old URL with the old query parameters. The legacy endpoint answers those calls, so you do not have to ship an update to every customer. It requires no API key: old clients never sent one.

The endpoint is off by default: a fresh install has nothing to stay compatible with. Turn it on with the switch in the Legacy Endpoint Configuration card header. All six options live on the Legacy Endpoint tab of Licenses → Settings (since v1.0.17; previously on the API Endpoints tab), and the reference on the API Endpoints tab shows the resulting URL and parameter names as you type. They can also be set in .env, which the admin settings override.

Setting Environment variable Description Default
legacy_api_enabled LICENSES_LEGACY_API_ENABLED Master switch. While it is off, the route is not registered, the dedicated domain below is not restricted, and the path is left out of the generated .htaccess / precheck rules. It still needs a path to answer. off
legacy_api_path LICENSES_LEGACY_API_PATH The URI the old clients call, relative to the site root (e.g. envato.php, verify.php, license-check). Required while the endpoint is enabled, nothing answers without it. envato.php
legacy_api_domain LICENSES_LEGACY_API_DOMAIN A domain dedicated to the API: every web page is blocked on it so it never serves duplicate content, and only the API endpoints answer there (e.g. api.example.com). Leave empty to serve the endpoint on the main domain only. (empty)
legacy_api_params.purchase_code LICENSES_LEGACY_PARAM_KEY Name of the query parameter carrying the license key or purchase code. purchase_code
legacy_api_params.domain LICENSES_LEGACY_PARAM_DOMAIN Name of the query parameter carrying the domain. domain
legacy_api_params.item_id LICENSES_LEGACY_PARAM_ITEM Name of the query parameter carrying the product item ID. item_id

Example: if your old clients call https://api.example.com/verify.php?serial=...&site=..., set the path to verify.php, the domain to api.example.com, the purchase code parameter to serial and the domain parameter to site. The three parameter names must be different from one another; an empty field keeps its default name.

Upgrading from v1.0.14 or earlier? The switch is seeded as on for an install that was already answering on a legacy path, so customers' software keeps verifying across the update. It is only off by default on installs that had no legacy path configured.
Changing the switch or the path rebuilds what runs before Laravel. The route table cache is cleared on save, and, when the .htaccess blocklist is enabled, the generated Apache rules and precheck gatekeeper are regenerated, because both match on the legacy path and parameter names.

Legacy endpoint banning rules Added v1.0.17

The Banning Rules tab holds one card per endpoint family. The second, Legacy Endpoint Banning Rules, gives the legacy endpoint rules of its own: the counterpart of the API Banning Rules card above it, which rules the /api/licenses/* endpoints alone. It is hidden while the legacy endpoint is off; turn the endpoint on from the Legacy Endpoint tab and it appears, with the values that were saved:

  • the two auto-ban on rate limit switches (strict, and unverified keys/domains only), each with its own per-minute and per-day thresholds. Legacy calls are counted on separate rate limiters, so a client hammering the legacy endpoint does not consume the non-legacy API budget of the same IP;
  • the oracle-safe ban response switch and its rejection message. The legacy default reads "License verification could not be completed with this version of the software. For security reasons, please update your installation to the latest release.": a legacy caller runs an outdated version of your software, so rather than revealing why the call was rejected, the message tells it to update. The non-legacy API keeps its neutral Request rejected. (editable on the API Banning Rules card as well).

Every legacy value follows the API one until it is set, so updating changes nothing: the fields show the API values, and saving the tab records them for the legacy endpoint. The message is the exception and has the default above. The precheck gatekeeper bakes both families' response settings in, and picks the one matching the request path.

Marketplace Settings

Each marketplace has its own card on the settings page. The enable switches are described in Marketplaces.

Setting Marketplace Description
envato_api_token Envato Market Personal token from build.envato.com
envato_author_username Envato Market Your Envato author username
envato_purchase_code_regex Envato Market Pattern a purchase code must match (default: the UUID format Envato uses)
gumroad_access_token Gumroad Access token of a Gumroad application (used by the connection test)
gumroad_product_ids Gumroad Comma-separated Gumroad product IDs to check keys against. Not needed while the Gumroad add-on is active: its imported products are used.
gumroad_license_key_regex Gumroad Pattern a Gumroad license key must match
webhook_secret Webhook Shared secret for HMAC-SHA256 signature verification

Marketplaces Updated v1.0.13

A marketplace is where a license key comes from. Every key records its marketplace, and every product records the marketplaces it is sold on. There are two kinds:

Marketplace Kind Where its keys come from
Shop (shop) Store Generated when an order of the in-site Shop add-on is confirmed (see Shop Auto-generation).
Envato Market (envato) Store Envato purchase codes, verified against the Envato API the first time they are seen.
Gumroad (gumroad) Store Gumroad license keys, verified against the Gumroad API the first time they are seen.
Manual (manual) Internal Keys you create or bulk-generate from the admin panel. Formerly called “In-site”.
Webhook (webhook) Internal Keys pushed by an external system to POST /api/licenses/callback/webhook.

A store is a place where products are sold, so a license product can be listed on it (see Where a Product Is Sold). Internal marketplaces have no listings.

Enabling a Marketplace

Go to Licenses → Settings. Each marketplace card has an Enable switch, and the Marketplace status card shows which ones are on.

Setting Environment variable Default When enabled
licenses_manual_marketplace_enabled LICENSES_MANUAL_MARKETPLACE_ENABLED ON Customers can register the keys you issued by hand.
licenses_shop_marketplace_enabled LICENSES_SHOP_MARKETPLACE_ENABLED ON A confirmed Shop order issues its license keys. Needs the Shop add-on.
licenses_envato_marketplace_enabled LICENSES_ENVATO_MARKETPLACE_ENABLED OFF Unknown keys are checked against the Envato API and saved when valid. Items imported by the Envato add-on are also listed on license products.
licenses_gumroad_marketplace_enabled LICENSES_GUMROAD_MARKETPLACE_ENABLED OFF Unknown keys are checked against the Gumroad API and saved when valid. Products imported by the Gumroad add-on are also listed on license products.

The enable switches also decide which marketplace sections the product form offers, and which marketplaces customers are told they can buy from.

Renamed in v1.0.13. These settings used to be called licenses_insite_provider_enabled, licenses_shop_provider_enabled, licenses_envato_provider_enabled and licenses_gumroad_provider_enabled. The update renames them and keeps their values. If you set them in .env, rename the variables too (LICENSES_*_MARKETPLACE_ENABLED).

Environment Variables

Admin: Dashboard

The dashboard (Licenses → Dashboard) provides a real-time overview:

  • Stats cards: Total keys, active keys, total products, active activations
  • Status breakdown: Suspended, expired, and revoked key counts
  • Recent keys: Last 10 created keys with masked display and status badges
  • Recent webhooks: Last 5 webhook events with marketplace and status

Admin: Products

Products represent the software or service being licensed. Each license key belongs to one product.

Creating a Product

Navigate to Licenses → Products → Add Product.

Field Required Description
name Yes Product name (translatable)
slug No URL-safe identifier. Auto-generated from name if empty. Unique.
slug_aliases No Other names client apps may send for this product, comma-separated. May be shared with other products. See Shared Slug Aliases.
description No Product description (translatable, max 2000 chars)
sku No The product’s own identifier, the one client apps send first. Must be unique. See The SKU.
version No Current product version (e.g. 2.1.0)
is_active - Toggle. Inactive products are hidden from selection lists.

The SKU: the Product Identifier Updated v1.0.13

The SKU is the product’s own identifier. The applications, add-ons and themes you sell send it first when they check a purchase code, so it must be the same string everywhere:

  • on the license product;
  • on the Shop product the license product is sold as;
  • in the sku key of the add-on’s addon.json or the theme’s theme.json (and in the app’s own license config).

Use a value namespaced per product line, so that two lines never collide. If you sell two applications, myapp and otherapp, name their SKUs myapp and otherapp, the PayPal add-on of each one myapp-paypal and otherapp-paypal, and a theme myapp-theme-aurora.

Never use a marketplace ID as the SKU. An Envato item ID or a Gumroad product ID belongs to the product’s listing on that marketplace (see Where a Product Is Sold), not to the SKU. The 1.0.13 update clears every SKU that still held an Envato or Gumroad ID, and gives every product without a SKU its slug as SKU.

The SKU field buttons

In the product create and edit forms, the SKU field has two buttons:

  • Shop button (shop icon, before the field, shown only while the Shop add-on is active): opens a searchable, paginated list of the shop products. Click one to copy its SKU into the field. A shop product without a SKU cannot be picked.
  • Magic button (after the field): finds the SKU of the shop product that matches this product, or generates a new one when there is no match (or no Shop add-on).

Keeping the Shop and license SKUs identical

When a license product is sold on the Shop, both SKUs are kept the same:

  • When one side has no SKU, saving either product copies the SKU from the other side.
  • Two different SKUs are never overwritten. licenses:doctor reports them (sku_mismatch), and the Create / Update license product button of the shop product refuses to run until you align them.
  • That button also refuses when the shop SKU is already used by another license product.

Where a Product Is Sold Added v1.0.13

A license product can be sold on several stores at once. Each store it is sold on is a listing, recorded with the identifiers that store knows the product by. A purchase code from any of these listings unlocks the product.

The product form has one section per store: Envato, Gumroad and Shop. A section opens by itself when the product is already listed on that store. Otherwise, click its Add button above the sections. An Add button is offered only while the marketplace is enabled in the settings.

Section Pick from the catalog Or type the identifiers
Envato An item imported by the Envato add-on Envato item ID
Gumroad A product imported by the Gumroad add-on Gumroad product ID, short permalink, custom permalink
Shop A product of the Shop add-on Shop product SKU
  • When the store’s add-on is installed, the section shows a searchable picker. The identifiers of the picked listing are saved with it. Click Enter the identifiers manually to type them instead.
  • When the store’s add-on is not installed, only the identifier fields are shown. The License Management add-on works without the Envato and Gumroad add-ons: the typed identifiers are then how it recognizes a purchase code.
  • Clearing the picker (or the fields) removes the listing.
  • The products list shows a badge for every store a product is sold on, and can be filtered by marketplace (or by “Not sold on any marketplace”).

The Links pages of the Envato and Gumroad add-ons edit the same listings, from the other side: pick the license products an Envato item or a Gumroad product sells. One listing may sell several license products (a bundle).

Shared Slug Aliases Updated v1.0.13

Older client apps identify a product only by its directory name (paypal, stripe…). Two of your product lines can ship an add-on under the same directory name: each of two applications you sell may have its own paypal add-on, sold as two license products. If only one of them could answer to paypal, a buyer of the other one would get “License key does not belong to this product.”

So a slug alias may be given to several products. Add paypal to the aliases of both PayPal products: an old client sending paypal then verifies a key of either one. The API still checks the key’s own product, so a key of the first product does not unlock anything else.

  • The primary slug stays unique.
  • An alias may appear only once in a product’s own list. An alias equal to the product’s slug is dropped.

How the API Recognizes a Product

A client app sends the product parameter with the purchase code. Current client apps try, in this order, until one is accepted:

  1. the product’s SKU;
  2. the product’s marketplace IDs whose format matches the code (the Envato item ID for an Envato purchase code, the Gumroad ID for a Gumroad key, the shop SKU for a shop key);
  3. the slug (directory name).

Older clients send only the slug, and keep working. On the server, the key’s product is accepted when the value is, in this order:

  1. its SKU;
  2. one of its slug aliases;
  3. the legacy item_id saved in its metadata;
  4. an identifier of one of its listings: Envato item ID, Gumroad product ID or permalinks, shop SKU;
  5. its primary slug.

Otherwise the API answers “License key does not belong to this product.”

Every one of those values is accepted, so what you send only changes which step matches:

What the client sends What happens
product=<SKU> Matched at step 1, the shortest path. This is what current client apps send first.
product=<alias slug> Matched at step 2, against the product’s alias list.
product=<marketplace ID> Matched at step 4 (an Envato item ID, a Gumroad product ID, a Gumroad custom or short permalink, or a shop product SKU) as long as the product has a listing carrying that identifier. If it is not listed on that marketplace, nothing matches and the call is rejected.
product=<slug> Matched at step 5, last. It still works, but every earlier step is tried first.
No product at all The product check is skipped: the key verifies as long as it exists, is active and its activation limit allows it. The answer still names the real product, so a client can compare it itself. Old software that sends its item ID under another parameter name, such as item_id=, falls in this case on this endpoint: that parameter is only read by the legacy verification endpoint, which resolves it to a product and does check it.
Two things to keep in mind.
  • A shared alias is deliberately ambiguous. The check only asks whether the key’s own product carries the value, so an alias held by several products (paypal on the PayPal add-on of two different applications) lets a key of either one verify. That is what fixes old clients, and it also means the alias alone does not prove which product the caller meant.
  • Matching is exact. Comparison is case-sensitive and a surrounding space makes it fail, so send the value exactly as it is saved on the product.

Add-on Integrations

License products connect to other add-ons through one-click bridge buttons. Each button only appears when the counterpart add-on is active and you have the relevant permission.

  • Import from Envato add-on and Import from Gumroad add-on (page toolbar, when that add-on is active): create or update license products from your imported Envato items or Gumroad products, and record each one as the product’s listing. Their per-item counterpart, a Create/Update license product button, lives on each item of the Envato and Gumroad add-ons. An item is matched to its existing product by its listing first, then by slug or name, so it never gets two products. Imported products are created without a SKU: set the SKU yourself.
  • Create/Update support department (row action + Edit modal footer, when the HelpDesk add-on is active): creates or updates a helpdesk department for the product and gates it behind license ownership. See the HelpDesk add-on docs for details. It is safe to click repeatedly: it updates the same department instead of creating duplicates.
  • Create/Update license product from a Shop digital product: the reverse direction: that button lives on the Shop product admin, creates or updates the license product, and records the shop product as its Shop listing. The license product gets the shop product’s SKU (see SKU sync).
No duplicates. Every bridge is idempotent: re-running it finds the existing record (by its listing, SKU, slug or name) and updates it rather than creating a second one.

Admin: License Keys

Creating a Key

Navigate to Licenses → License Keys → Add Key.

Field Required Description
product Yes The product this key belongs to
key No License key string. Leave empty to auto-generate on save, or click the Generate (wand) button in front of the field to generate one now (see below).
key_format No Format used to generate the key: both by the Generate button and on save (see Key Formats & Prefix)
license_type Yes Standard, Extended, Trial, or Lifetime
status Yes Active, Suspended, Expired, or Revoked
expires_at No Expiration date. Leave empty for non-expiring keys.
supported_until No Support expiry date. Distinct from license expiry: tracks when the customer’s support period ends (e.g. Envato 6-month support window).
max_activations Yes How many domains/machines this key can be activated on simultaneously
marketplace Yes Where the key comes from: manual, shop, envato, gumroad or webhook (see Marketplaces)
marketplace_reference No External order ID, purchase code, etc.
assigned_user No Optionally link to a user account
notes No Internal notes (not exposed via API)

Generating the key by hand

The Key field carries a Generate button (a wand icon) in front of it. Clicking it fills the field immediately with a fresh key built in the Key Format currently selected just below, so you can read the key, copy it, or adjust the format and try again before saving. The field stays editable: you can still type a key of your own over it.

Both ways of getting a key end up identical:

  • Leave the field empty → the key is generated when the form is saved, in the selected format.
  • Click the Generate button → the same key is generated right away and shown in the field, then stored as-is on save.

The button asks the server for the key rather than building it in your browser, so the generated key is always a real one: the prefixed format gets the prefix configured in Settings → Key Generation, the hmac_signed format gets a genuine signature, and the result is checked against the existing keys so it can never collide with a key already issued.

Tip: Clicking the button again replaces whatever is in the field, nothing is saved until you submit the form, so press it as often as you like. Change the Key Format first if you want a different shape.
Note: The key string cannot be changed after creation. Choose your format carefully, or let the system auto-generate it.

Key Formats & Prefix

Every generated key follows one of four formats. The format is picked per key on the create and bulk-generate forms; the one preselected there comes from Settings → Key Generation → Default Key Format.

Format Shape Example
uuid A standard UUID v4 (36 characters, lowercase hexadecimal). 550e8400-e29b-41d4-a716-446655440000
alphanumeric Five groups of five uppercase letters and digits, separated by dashes. A1B2C-D3E4F-G5H6J-K7L8M-N9P0Q
prefixed Your own prefix, then four groups of five uppercase letters and digits. LIC-A1B2C-D3E4F-G5H6J-K7L8M
hmac_signed Two groups of five characters, a dot, and a 12-character signature computed with the HMAC key. A1B2C-D3E4F.a3f2b1c4d5e6

The Key Prefix

The Key Prefix field (Settings → Key Generation) is the leading word of the prefixed format: the LIC in LIC-A1B2C-D3E4F-G5H6J-K7L8M. Use it to make your keys recognisable at a glance, for instance your brand or the product line they belong to.

  • It is only used by the prefixed format. Keys generated as uuid, alphanumeric or hmac_signed ignore it entirely.
  • Letters only, up to 10 characters. It is uppercased when the key is built, so acme produces ACME-….
  • Because it is only useful for that one format, the field is revealed only when Default Key Format is set to Prefixed. Your prefix is kept while it is hidden: switch the format back and the value you saved is still there.
  • It applies to every prefixed key generated afterwards: from the create form, the Generate button, bulk generation, and automatic generation on a shop order.
Change the prefix before you start selling, not after. The prefix is also part of the pattern the API uses to recognise a key of the prefixed format. If you change it once prefixed keys are in your customers’ hands, those older keys no longer match the pattern and are rejected by verify / activate with “The license key format does not match any enabled marketplace.”, before the key is even looked up. If you must change it, generate replacement keys for the customers holding the old ones.

Bulk Operations

Bulk Generate

Navigate to License Keys → Bulk Generate to create up to 1,000 keys at once. Select the product, format, license type, max activations, and optional expiration date.

Bulk Delete

On the keys list page, select multiple keys with checkboxes and click Delete Selected.

CSV Export

Click Export CSV on the keys list page. Supports filtering by product and status before export.

Lifecycle Actions

Action Effect
Suspend Sets status to suspended. The key fails verification but can be reactivated later. Existing activations remain but are non-functional.
Revoke Sets status to revoked and deactivates all active activations. This is irreversible in practice.
Reactivate Sets status back to active. The key becomes valid again (if not expired).

Admin: Activations

View all activations for a specific key by clicking Activations on the key detail page, or navigating to /admin/licenses/keys/{id}/activations.

Each activation record shows:

  • Domain (if provided), with a Local badge for local/development domains
  • Machine ID (if provided)
  • Version Added v1.0.17: the version of the app, add-on or theme the client reported with its version parameter, refreshed on every later verify / activate call, so an audit tells which installs still run an outdated release. Larapen, LaraClassifier and JobClass releases that include this change send it for the app itself and for every add-on or theme they activate; older clients leave it empty. The search box matches it, and the API log shows it next to the product of every call.
  • IP address
  • User agent
  • Activation date
  • Active/Deactivated status

Admins can manually deactivate any active activation to free up a slot for the customer.

When “Record local domain activations” is enabled in the Local Domain Bypass settings, local activations appear in the list but do not count toward the key’s max_activations limit. A Delete local activations button allows bulk-removing all local activation records for a key. The license keys index page provides a filter to show keys with local activations only.

Admin: Settings

The settings page (Licenses → Settings) is organized into sections:

  • API Configuration: for the /api/licenses/* endpoints, the API enable switch, the API key, the rate-limit bypass token and the maintenance-mode exemption
  • Banning Rules (named Blocklist before v1.0.17, and .htaccess Blocklist before that): one card per endpoint family: API Banning Rules and Legacy Endpoint Banning Rules, each with its two auto-ban on rate limit switches, their per-minute + per-day thresholds and the oracle-safe ban response with its rejection message, followed by the IP banning switch, the non-legacy API ban exemption, the required request parameters (block API calls that send no license key, and/or no domain, as if they were banned) and the .htaccess blocklist, enable/disable, strategy, max entries per list (default 500), Regenerate/Remove/View buttons
  • Legacy Endpoint: the legacy endpoint configuration: switch, path, dedicated domain and parameter names. Its banning rules live on the Banning Rules tab, beside the API ones
  • Key Generation: Default key format, key prefix (shown only while the default format is Prefixed, since no other format uses it), default max activations and the HMAC key
  • Domains: how the domain a client sends is matched against recorded activations: “Treat www. as the same domain” (on by default: www.toto.com and toto.com share one activation slot) and “Allow subdomains of activated domains” (off by default: when on, toto.com, fr.toto.com and de.toto.com share one activation whichever was activated first), plus the Local Domain Bypass: enable/disable bypass, record local activations toggle, and domain patterns
  • Marketplaces: one card each for Manual, Shop, Envato Market and Gumroad, with its Enable switch and its credentials (see Marketplaces)
  • Webhook marketplace: Webhook secret and URL display
  • Cache: Enable/disable caching (default TTL: 24 hours, max 7 days) + Clear Licenses Cache button
  • API Call Logging: Enable/disable, retention period (days), optional max entries cap
  • API Endpoints: the reference for every endpoint the add-on answers, its HTTP method, its full URL (with a copy button), whether it expects the X-Api-Key header, and each parameter with its required/optional state; the legacy entry mirrors the Legacy Endpoint tab as you type

Admin: Webhook Logs

All incoming webhook events are logged with their marketplace, event type, raw payload, response data, status (Success / Failed / Ignored), and any error messages. Navigate to Licenses → Webhook Logs to review. Filter by marketplace or status.

Admin: API Logs Added v1.0.1

Every incoming API call to verify, activate, deactivate, callback, and the legacy endpoint is recorded in the licenses_api_logs table for debugging and auditing. Navigate to Licenses → API Logs.

What is logged

  • Endpoint (route name), HTTP method, status code, success flag
  • IP address, user agent, duration (ms)
  • Full request payload (query + body) with sensitive fields (api_key, password, token, secret, authorization) automatically redacted as [REDACTED]
  • Response payload (truncated to max_body_size if too large)
  • Error message extracted from the response body when the call is not a business-level success
  • Masked API key (abcd...wxyz format): the full key is never stored

Filters & Features

  • Filter by endpoint, status (success/failed), date range, domain, or key
  • "Successful / Failed" stat cards at the top
  • Per-row details modal with full request/response JSON, ban/unban actions, quick-filter icons, and an "Open verification URL" link
  • Bulk delete, "Clear all", and "Prune old" actions

Group calls Added v1.0.16

One thing a customer does can make several API calls. Activating a paid add-on with a purchase code is the clearest case: the client verifies that one code against each identifier the product answers to (its SKU first, then the marketplace identifiers whose format matches the key, then its slug) and stops at the first one that recognizes it. A single activation therefore fills the log with up to four near-identical rows a few seconds apart.

The Group calls switch in the filter bar folds them back together: one entry per client action and per product the calls turned out to be about. An entry shows the outcome of its last call and a badge counting the calls it folds; expanding it lists each call with the identifier that was sent, named for what it is (SKU, Gumroad ID, Shop SKU, Slug), its status, its error and its own actions. Two entries for one activation usually means the identifiers did not all resolve to the same product: worth a look at the product's slug aliases.

Calls are tied together by key, caller (domain or machine ID), IP and endpoint, within a window of api_log.correlation_window_seconds (default 120). Raise it for slow clients, lower it to split entries more eagerly. The switch only changes the display: every call stays its own row in the table, and turning the switch off shows them all again.

Retention & Cleanup

  • The scheduled task licenses:prune-api-logs runs daily and deletes logs older than the configured retention period (default: 30 days).
  • Optional "Max entries" cap: when enabled, oldest logs are deleted once the table exceeds the configured size. Also enforced probabilistically (~1% of inserts) in real-time.

What is NOT logged (to protect database performance)

  • Banned requests (403): When a request is rejected by the middleware as a banned IP/key/domain, no log row is written: ban detection is a pure cache lookup, so no DB activity occurs.
  • Rate-limited (429) and validation failures (422): Only the first occurrence per key+domain+machine_id combo is logged within a 5-minute window. This prevents flooding the log table when thousands of different IPs hit the same key.

Admin: Blocklist (Bans) Added v1.0.1/1.0.2

The blocklist lets admins block abusive traffic at three levels. Checks run in the CheckBannedKey middleware, applied to all API routes (non-legacy + legacy). All ban lookups are served from the cache with zero DB queries.

Navigate to Licenses → Blocklist, the page has three tabs: Banned Keys, Banned Domains, Banned IPs. Each tab has a search box and a "Ban a..." modal. The Banned IPs tab only appears while IP banning is enabled (it is off by default).

Banned Keys

Block a specific license key or Envato purchase code. Any future API request with that key in key or the configured legacy param (default: purchase_code) returns HTTP 403 with "error": "key_banned".

Banned Domains

Block a specific domain value. Any request with that domain in the domain param (non-legacy API or legacy custom name) returns HTTP 403 with "error": "domain_banned". Matching is case-insensitive.

Banned IPs

Block a specific source IP. Evaluated before key/domain checks. Returns HTTP 403 with "error": "ip_banned". Requires the IP banning option below to be enabled.

IP Banning Switch Added v1.0.9

A master switch for the IP axis of the blocklist, under Settings → Banning Rules. It is disabled by default, because an abusive IP is often a shared or NAT address and banning it can lock out unrelated customers.

While it is off:

  • No request is ever rejected because of its source IP: the middleware skips the IP check entirely
  • No IP is written to the generated .htaccess rules or precheck files (they are rewritten when you flip the switch)
  • Auto-ban on rate limit never falls back to banning an IP
  • The Banned IPs tab is hidden, and so are the ban/unban IP actions in the API logs
Existing bans are never deleted: they stay in licenses_banned_ips and are enforced again the moment the option is switched back on.

The .htaccess rules and the precheck files are generated snapshots, so saving the settings reconciles them with what the switches now say: they are regenerated when the IP banning switch flips while the blocklist is enabled, and a blocklist left applied after the blocklist option was turned off is removed (rules generated earlier would otherwise keep rejecting the banned IPs before PHP runs). If those files cannot be written, the save reports a warning instead of failing silently.

Each ban can include an optional reason (up to 2000 chars) and is stamped with the admin who performed it. Bans can be performed from the blocklist page or directly from the API logs detail modal (buttons at the modal bottom).

Required Request Parameters Added v1.0.12

Two switches under Settings → Banning Rules → Required request parameters, both off by default:

  • Block requests without a license key: rejects verify / activate / deactivate and legacy calls that send no key parameter (or the legacy purchase code parameter). Such calls could only ever fail validation, so they are almost always probes, scanners or misconfigured clients.
  • Block requests without a domain: rejects calls that send no domain parameter (or the legacy domain parameter).

A rejected call is treated exactly like a banned one: same HTTP 403, same payload (the oracle-safe generic response when that option is on, otherwise key_missing / domain_missing), and no row in the API logs. The rule is enforced at every layer of the blocklist:

  • always by the CheckBannedKey PHP middleware;
  • while the .htaccess blocklist is enabled, also before Laravel loads (as Apache rules in .htaccess mode, or inside the public/licenses.php gatekeeper in precheck mode), so the calls never reach the application. Saving the settings regenerates the rules automatically; the block is written even when no IP, domain or key is banned.
Domain rule and desktop clients: clients that identify themselves by machine_id only send no domain, so they are rejected too once the domain switch is on. Leave it off if you license desktop software.
In .htaccess mode Apache can only inspect the query string, so the rules apply to GET requests at the Apache layer; a POST body (activate / deactivate) is still checked by the PHP middleware. The precheck gatekeeper checks every request, JSON bodies included. The webhook callback endpoint is never affected.

Non-legacy API Ban Exemption Added v1.0.17

The Never ban callers of the non-legacy API switch under Settings → Banning Rules, off by default. While it is on, a call to /api/licenses/verify, /activate or /deactivate:

  • is never auto-banned, whatever rate limit it trips;
  • is never rejected by a ban: the banned IP, key and domain checks let it through;
  • lifts any ban already recorded on the key, the domain or the IP it carries, the moment it arrives (a ban on another host of the same registrable domain that covers it, under the Allow subdomains policy, is lifted too). Bans made by hand from the Blocklist page are lifted the same way.

Only the legacy endpoint keeps banning and enforcing bans. It is meant for installs where the non-legacy API is only called by known client apps (each sends the X-Api-Key header) while the legacy endpoint is the one exposed to scanners.

The required request parameters rules are not bans and still apply to every endpoint. While the .htaccess blocklist is enabled, the generated ban rules are limited to the legacy endpoint path (none is written when the legacy endpoint is off) and the precheck gatekeeper skips the ban checks for the non-legacy endpoints; saving the settings regenerates both, and a lifted ban queues a regeneration like an automatic ban does.

Auto-ban on Rate Limit Added v1.0.2

Two switches per endpoint family, both on the Settings → Banning Rules tab: the API Banning Rules card for the non-legacy API, and the Legacy Endpoint Banning Rules card for the legacy endpoint (since v1.0.17, with its own thresholds). When enabled, any request that exceeds the matching per-minute or per-day rate limit automatically creates a ban record:

  1. If the request includes a domain → the domain is banned
  2. If the request includes a key/purchase_code → the key is banned
  3. If neither domain, key, nor machine_id is present → the IP is banned
  4. If .htaccess blocklist is enabled, the Apache rules are regenerated automatically

.htaccess Blocklist Added v1.0.2

An optional Apache-layer defense. Blocked requests return 403 before PHP is even loaded: zero CPU, zero memory, zero DB. Critical when abuse traffic is high enough to saturate PHP-FPM workers.

Configure under Settings → Banning Rules:

  • Enable / Disable toggle
  • Blocking strategy (two modes, pick one based on how many bans you have):
    • Apache .htaccess rules: regex rules written directly into public/.htaccess. Fastest for small lists (<1000 bans). Apache re-parses .htaccess on every request, so performance degrades at scale.
    • PHP precheck file (recommended for large lists): deploys a tiny public/precheck.php gatekeeper that loads compiled PHP array files and does O(1) hash lookups. Scales to 100,000+ entries without performance loss thanks to OPcache. Slightly slower than .htaccess for very small lists, but dramatically faster beyond 1,000 entries.
  • Max entries per list: cap on how many bans are written (htaccess: 10-5000, precheck: 100-1,000,000). Entries beyond the cap remain enforced by the PHP middleware.
  • Regenerate Rules: reads current bans and rewrites the blocklist files
  • Remove Rules: strips the .htaccess block and deletes any precheck files
  • View .htaccess Content: opens a modal showing the current file contents
Which mode should I pick? Start with .htaccess rules (simpler). Switch to PHP precheck file if you accumulate more than ~1000 bans or see the page load slowing down. Switching is safe: clicking Regenerate cleans up the previous mode's artifacts automatically.
Overflow warning: If any list exceeds the max entries cap, a yellow alert appears on the settings page with counts of written vs PHP-only entries. For extreme cases (millions of bans) consider moving IP banning to iptables/ipset or a CDN/WAF (Cloudflare).

Rate limit bypass token

An optional secret token configured under Settings → API Configuration. When present as ?bypass_token=... (or X-Bypass-Token header) and matching the stored value, the rate limiter returns Limit::none(): used for admin re-testing rate-limited requests. Rate-limited log entries in the API logs modal automatically use a URL with the bypass token appended.

Admin: Cache Added v1.0.1

All the add-on's hot-path database reads (findByKey, verify, getStats, getActiveProducts, banned IP/domain/key lists, etc.) are served from a centralized cache (LicenseCacheService). This keeps API verification free of DB queries once warm.

  • Default TTL: 24 hours (configurable 1-168 hours from Settings)
  • Auto-invalidation: Any write to a license key, product, activation, or banned record clears the whole licenses cache via the observer pattern
  • Clear cache button: Clears the licenses cache without affecting other system caches
  • Negative results are NOT cached: findByKey/verify cache only valid keys, "not found" results are never cached, so a key created later (by the Envato or Gumroad marketplace, or by an admin) is found immediately

Updating

There are two ways to update this add-on: via the admin panel (recommended) or manually replacing files.

Method 1: Admin Panel Upload (Recommended)

  1. Download the latest .zip file of this add-on.
  2. Go to Admin panel → Add-ons and click the Upload button.
  3. Select or drag the .zip file into the upload area.
  4. A confirmation prompt will show the current and new version numbers. Click Replace to proceed.
  5. Go to Admin panel → System Update (/admin/update) to apply any pending database migrations.

Method 2: Manual File Replacement

Step 1: Replace Files

Replace the add-on directory with the new version (or pull the latest if using a symlink to a Git repository).

Step 2: Run Migrations

New migrations are automatically picked up. The add-on uses timestamped migrations that only run once.

Step 3: Clear Caches

php artisan config:clear
php artisan route:clear
php artisan view:clear

Step 4: Verify

Visit Licenses → Dashboard to confirm everything loads correctly. Check the Settings page for any new configuration options introduced in the update.

Backup first: Always back up your database before running migrations on a production system.

Updating to v1.0.13: Update Order Action required

Version 1.0.13 moves every “product X is sold on store Y” link into the License Management add-on. The Envato and Gumroad add-ons changed with it. Apply the updates in this order:

  1. License Management 1.0.13 first. Its update copies the links still kept by the Shop product column, by the Envato and Gumroad link tables and by the product metadata into the new listings, and renames the marketplace settings.
  2. Then Envato 1.0.6 and Gumroad 1.0.3. They copy any remaining links, then drop their own link tables (envato_item_links, gumroad_product_links). They drop them only once the License Management tables exist.

After the updates:

  • Rename any LICENSES_*_PROVIDER_ENABLED variable in .env to LICENSES_*_MARKETPLACE_ENABLED (the insite one becomes LICENSES_MANUAL_MARKETPLACE_ENABLED).
  • Check the SKU of your products: SKUs that held an Envato or Gumroad ID were cleared, and products without a SKU received their slug (see The SKU).
  • Run php artisan licenses:doctor and fix what it reports (see Link Audit).

Link Audit (licenses:doctor) Updated v1.0.13

A listing decides who gets access: a purchase code from it unlocks the license product and everything gated behind it. The licenses:doctor command lists the links that look wrong. It changes nothing.

php artisan licenses:doctor
CheckSeverityWhat it means
title_mismatchWarningThe listing is sold under another title than the product. It was renamed, or the listing is the wrong one.
source_without_linkWarningThe product was imported from a store but is not listed on it, so purchases made there cannot be matched to it.
orphan_linkErrorThe listing points at a license product or a store catalog row that no longer exists.
shared_listingWarningOne listing sells several products: a single purchase code unlocks all of them.
entity_unreachable_from_storeWarningA gated support department, knowledge base collection or forum category is closed to a store’s buyers: none of its products is sold there.
store_not_wiredWarningA store is open to customers but no product is listed on it, so every gated entity is closed to its buyers.
sku_mismatchWarningA shop product and its license product carry two different SKUs.
sku_missingWarningA product is sold but has no SKU. Give it the SKU its addon.json / theme.json declares.

Options

  • --threshold=: title similarity (%) below which a listing is reported (default 60)
  • --errors-only: report only the findings that are certainly broken
  • --acknowledge: walk the findings and silence the ones you confirm are intentional
  • --include-acknowledged: also show the silenced findings
  • --forget: walk the silenced findings and bring back the ones you choose
  • --strict: exit with an error code on warnings too
  • --json: output the findings as JSON

Merging two products

When the same software was imported once per store, you get one product per store. Merge them so one product carries every listing:

php artisan licenses:merge-products {from} {into} --dry-run

{from} and {into} are product IDs or slugs. The command moves the keys, the gated-entity links and the listings of {from} to {into}, then deletes {from}. Drop --dry-run to apply it (--force skips the confirmation).

License Registration Page

A public-facing page where customers register their purchase code and activate it on their domain. The page lets the customer choose where the code comes from, among the enabled marketplaces (Manual, Envato Market, Gumroad).

Available at /{locale}/licenses/register (or /licenses/register without locale prefix).

URL & Routes

MethodURLRoute NameDescription
GET /{locale}/licenses/register front.licenses.register.localized Show registration form
POST /{locale}/licenses/register front.licenses.register.localized Process registration
GET /licenses/register front.licenses.register Non-localized variant

Registration Flow

  1. Customer visits /licenses/register and fills in their license key / purchase code and domain.
  2. The system searches for the key in the local database.
  3. If not found locally, it tries each enabled and configured marketplace (Envato Market, Gumroad): the code is verified against that marketplace’s API. If valid, a license key is created with that marketplace (for example marketplace = envato).
  4. If the user is authenticated, the LicenseKey is linked to their user account (user_id).
  5. The system calls LicenseActivationService::activate() with the provided domain.
  6. On success, the customer sees a confirmation message. On failure (invalid key, expired, max activations), an error is displayed.

Form Fields

FieldValidationDescription
license_key Required, string, max 255 The license key or Envato purchase code
domain Required, string, max 255 The domain where the software is installed (without http/https)

Theme Views

Each theme provides its own styled version of the registration form at:

Themes: default, creative, elegant, minimalist, olive, technology.

Tip: The registration page is linked from the front-end navigation menu via the front_menu entry in addon.json. The label “Register License” appears in the header menu.

Customer Portal (My Licenses)

Authenticated customers can view all their linked licenses and manage domain activations. Requires user login (uses auth middleware).

URL & Routes

MethodURLRoute NameDescription
GET /{locale}/licenses/list front.licenses.my.localized List all user’s licenses
GET /{locale}/licenses/list/{id} front.licenses.my.show.localized View license details & activations
POST /{locale}/licenses/list/{id}/deactivate front.licenses.my.deactivate.localized Deactivate a domain

Non-localized variants (without {locale}) are also available.

My Licenses Page

Displays a list/table of all licenses owned by the logged-in user, showing:

  • Product name: linked license product
  • License key: masked by default (e.g., XXXX-XXXX-XXXX-ab12), with a copy button
  • Status badge: Active (green), Expired (amber), Suspended (red), Revoked (dark)
  • License type: Standard, Extended, Trial, Lifetime
  • Activations: count / max (e.g., “2 / 3”)
  • Expiry date: or “Never” for lifetime licenses
  • View Details link

License Detail Page

Shows complete information for a single license:

  • Key Information: full key (masked with reveal toggle via vanilla JS), status, type, issued/expiry dates
  • Active Domains table: domain, IP address, activation date, and a Deactivate button for each
  • Activation History: all activations (active + deactivated) with timestamps

Deactivation

Customers can self-deactivate domains they no longer use. The deactivation:

  1. Verifies the license belongs to the authenticated user
  2. Calls LicenseActivationService::deactivateByDomain()
  3. Frees up an activation slot for use on another domain
  4. Redirects back with a success message

Theme Views

User Menu: The “My Licenses” link appears in the user account dropdown menu (configured via user_menu in addon.json, icon: bi-key).

Shop Auto-generation

When the Shop add-on is active, the Shop marketplace is enabled, and a shop product is listed on a license product, license keys are automatically generated when an order is confirmed.

Setup

  1. Enable the Shop marketplace in Licenses → Settings (on by default).
  2. List the shop product on a license product, in either direction:
    • on the shop product, click Create / Update license product; or
    • in Licenses → Products, edit the license product and pick the shop product in its Shop section (see Where a Product Is Sold).
  3. Check the SKUs: the shop product and the license product must carry the same SKU (see SKU sync).

Auto-generation Flow

  1. A customer completes a purchase via the shop.
  2. The order’s payment_status becomes paid, or its status becomes processing or completed (for example an accepted Cash on Delivery order).
  3. The ShopOrderObserver detects the change.
  4. For each order item whose shop product is listed on a license product:
    • A license key is created per item quantity (e.g., qty 2 → 2 keys)
    • Its marketplace is shop
    • Status is set to active
    • marketplace_reference is set to shop_order:{order_number} (prevents duplicate generation)
    • The key uses the UUID format
  5. A LicenseKeyIssuedNotification email is sent to the customer with:
    • Greeting with the customer’s name
    • All generated license keys
    • A “Register License” button linking to /licenses/register
    • Instructions on how to activate

Duplicate Prevention

The observer checks for existing keys with the same marketplace_reference before generating. If keys for shop_order:ORD-12345 already exist, the observer skips generation. This prevents duplicates if the payment status is updated multiple times.

Refunds and Cancellations

When a confirmed order is refunded, cancelled, or its payment fails or goes back to pending, its active keys are suspended and the customer is notified. Confirming the order again reactivates the same keys.

Changed in v1.0.13: the link between a shop product and a license product is now its Shop listing. The former shop_products.license_product_id column is dropped by the update, after its links were copied into the listings.
Requirement: The Shop add-on must be installed and active.
Note: Keys are issued when an existing order changes (not when it is created). An order that is created directly as paid does not issue keys: its status must change to paid, processing or completed afterwards.

Uninstallation

Switching an add-on off without losing anything is a deactivation: go to Admin panel → Add-ons, find License Management and click Deactivate.

  • Its routes, views, admin menu entries and permissions stop being registered, and its front-end pages stop answering.
  • Its database tables and all the data they hold are kept, and its files stay under extensions/addons/licenses/. Nothing is deleted.
  • The purchase code recorded at activation is kept too, so activating the add-on again does not ask for it.
  • Deactivation is refused while another active add-on depends on this one: deactivate that add-on first.

Click Activate on the same card to switch it back on. Pending migrations are re-run, assets are republished, and the add-on picks up exactly where it left off.

Removing

Removing is permanent and destroys the add-on's data. The Remove button only appears on a deactivated add-on, so removal is always two steps:

  1. Deactivate License Management (see Uninstallation).
  2. Click Remove on its card and confirm the prompt.

The admin panel then, in one pass:

  • runs the add-on's uninstall hook, if it ships one, while its code is still on disk;
  • revokes the permissions declared in its addon.json;
  • rolls back its migrations (this drops its database tables and every row they hold) and purges its entries from the migrations table, so a later reinstall migrates from scratch;
  • deletes its published assets: public/addons/licenses/, public/vendor/licenses/ and storage/app/public/addons/licenses/;
  • deletes the add-on directory extensions/addons/licenses/;
  • deletes its row in the addons table (the recorded purchase code goes with it) and clears the application cache.
This cannot be undone. Back up your database before removing an add-on whose data you may still need: installing it again later creates empty tables, not your old content.

Removal is refused, with an explanatory message and before anything is destroyed, when the add-on is still active, when another active add-on depends on it, or when the web server (PHP) user cannot delete extensions/addons/licenses/. In that last case, give that user write permission on the directory and on its parent, then try again.

Deleting the folder over FTP or SSH is not equivalent: the add-on's tables, its entries in the migrations table and its addons row are all left behind, and its card stays in the list. Use Remove in the admin panel instead.

Troubleshooting

API returns 503: "License API is not configured"

You have not set an API key. Go to Licenses → Settings and set the API Key field, or set LICENSES_API_KEY in .env.

API returns 401: "Invalid API key"

The X-Api-Key header or api_key parameter does not match the configured key. Check for trailing spaces or encoding issues.

Activation fails with "Maximum activations reached"

The key has reached its max_activations limit. Options:

  • Deactivate an unused domain via POST /deactivate or from the admin panel
  • Increase max_activations on the key in the admin panel

Envato verification fails

Check that:

  • The Envato API token has View Your Envato Account Username and Verify Purchases permissions
  • The Author Username matches your Envato profile exactly (case-sensitive)
  • The purchase code is valid and belongs to one of your items

Webhook events are logged as "Failed"

Common causes:

  • Invalid signature: The X-Webhook-Signature header does not match. Ensure both sides use the same secret and compute HMAC-SHA256 over the raw request body.
  • Missing product: The product slug in the payload does not match any active product. Create the product first.
  • Invalid event: The event field must be one of: license.created, license.updated, license.revoked, license.expired.

Registration page returns “Invalid license key”

The key was not found locally and Envato verification also failed. Check:

  • The Envato API token and author username are correctly configured in Licenses → Settings
  • The purchase code is valid and belongs to one of your Envato items
  • If using manual keys, ensure the key exists in Licenses → License Keys

Local domain not being bypassed

Check that:

  • Local Domain Bypass is enabled in Licenses → Settings (or LICENSES_LOCAL_DOMAIN_BYPASS=true in .env)
  • The domain matches one of the configured patterns (patterns use fnmatch() syntax)
  • The domain does not include the protocol: use localhost not http://localhost

Shop purchase does not generate license keys

Check that:

  • The Shop marketplace is enabled in Licenses → Settings
  • The shop product is listed on a license product (its Shop section in the product form, or the Create / Update license product button of the shop product)
  • The order’s payment status is being changed to paid (or its status to processing / completed)
  • Check the marketplace_reference column: if keys with shop_order:{order_number} already exist, the observer skips generation

“License key does not belong to this product”

The key is valid, but the product value the client sent does not match the key’s product. Check that:

  • the product’s SKU is the one declared by the client’s addon.json / theme.json (sku key);
  • the product is listed on the store the key comes from, with the right identifiers (see Where a Product Is Sold);
  • for older clients that send only the directory name: the product has that name as its slug or as a slug alias. When two product lines share a directory name, give the alias to both products (see Shared Slug Aliases).

See How the API Recognizes a Product.

“Create / Update license product” refuses to run on a shop product

  • The SKUs differ: the shop product and its license product carry two different SKUs. Make them identical, then click again.
  • The SKU is already used: another license product already has the shop product’s SKU. Change one of them, or merge the two license products.

Client-side verification shows “License Invalid”

Check that:

  • LICENSE_API_BASE_URL points to the correct server (e.g., https://your-site.com/api/licenses)
  • LICENSE_API_KEY matches the LICENSES_API_KEY on the server
  • The purchase code is valid and has not been revoked
  • The server is reachable from the client (no firewall/DNS issues)

License Management Add-on v1.0.0: Part of the Larapen CMS platform.

© BeDigit. All rights reserved.

Was this article helpful?

Thank you for your feedback!

Still need help? Create a support ticket

Create a Ticket
Apr 07, 2026