API
SMM panel API
Connect your own panel or script to LiberLite. Compatible with the standard SMM panel API v2, so existing reseller scripts work by changing the URL and key.
https://liberlite.com/api/v2Service list
| Parameters | Description |
|---|---|
key | Your API key |
action | services |
Example response
[
{
"service": 1,
"name": "Followers",
"type": "Default",
"category": "First Category",
"rate": "0.90",
"min": "50",
"max": "10000",
"refill": true,
"cancel": true,
"dripfeed": false
},
{
"service": 2,
"name": "Comments",
"type": "Custom Comments",
"category": "Second Category",
"rate": "8",
"min": "10",
"max": "1500",
"refill": false,
"cancel": true,
"dripfeed": false
}
]
Add order
| Parameters | Description |
|---|---|
key | Your API key |
action | add |
service | Service ID |
link | Link to page |
quantity | Needed quantity |
runs (optional) | Runs to deliver |
interval (optional) | Interval in minutes |
| Parameters | Description |
|---|---|
key | Your API key |
action | add |
service | Service ID |
link | Link to page |
| Parameters | Description |
|---|---|
key | Your API key |
action | add |
service | Service ID |
link | Link to page |
comments | Comments list separated by \n |
| Parameters | Description |
|---|---|
key | Your API key |
action | add |
service | Service ID |
link | Link to page |
comments | Comments list separated by \n |
| Parameters | Description |
|---|---|
key | Your API key |
action | add |
service | Service ID |
link | Link to page |
quantity | Needed quantity |
usernames | Usernames list separated by \n |
hashtags | Hashtags list separated by \n |
| Parameters | Description |
|---|---|
key | Your API key |
action | add |
service | Service ID |
link | Link to page |
usernames | Usernames list separated by \n |
| Parameters | Description |
|---|---|
key | Your API key |
action | add |
service | Service ID |
link | Link to page |
quantity | Needed quantity |
hashtag | Hashtag to scrape usernames from |
| Parameters | Description |
|---|---|
key | Your API key |
action | add |
service | Service ID |
link | Link to page |
quantity | Needed quantity |
username | URL to scrape followers from |
| Parameters | Description |
|---|---|
key | Your API key |
action | add |
service | Service ID |
link | Link to page |
quantity | Needed quantity |
media | Media URL to scrape likers from |
| Parameters | Description |
|---|---|
key | Your API key |
action | add |
service | Service ID |
link | Link to page |
quantity | Needed quantity |
username | Username of the comment owner |
| Parameters | Description |
|---|---|
key | Your API key |
action | add |
service | Service ID |
link | Link to page |
quantity | Needed quantity |
answer_number | Answer number of the poll |
Example response
{
"order": 23501
}
Order status
| Parameters | Description |
|---|---|
key | Your API key |
action | status |
order | Order ID |
Example response
{
"charge": "0.27819",
"start_count": "3572",
"status": "Partial",
"remains": "157",
"currency": "USD"
}
Multiple orders status
| Parameters | Description |
|---|---|
key | Your API key |
action | status |
orders | Order IDs (separated by a comma, up to 100 IDs) |
Example response
{
"1": {
"charge": "0.27819",
"start_count": "3572",
"status": "Partial",
"remains": "157",
"currency": "USD"
},
"10": {
"error": "Incorrect order ID"
},
"100": {
"charge": "1.44219",
"start_count": "234",
"status": "In progress",
"remains": "10",
"currency": "USD"
}
}
Create refill
| Parameters | Description |
|---|---|
key | Your API key |
action | refill |
order | Order ID |
Example response
{
"refill": "1"
}
Create multiple refill
| Parameters | Description |
|---|---|
key | Your API key |
action | refill |
orders | Order IDs (separated by a comma, up to 100 IDs) |
Example response
[
{
"order": 1,
"refill": 1
},
{
"order": 2,
"refill": 2
},
{
"order": 3,
"refill": {
"error": "Incorrect order ID"
}
}
]
Get refill status
| Parameters | Description |
|---|---|
key | Your API key |
action | refill_status |
refill | Refill ID |
Example response
{
"status": "Completed"
}
Get multiple refill status
| Parameters | Description |
|---|---|
key | Your API key |
action | refill_status |
refills | Refill IDs (separated by a comma, up to 100 IDs) |
Example response
[
{
"refill": 1,
"status": "Completed"
},
{
"refill": 2,
"status": "Rejected"
},
{
"refill": 3,
"status": {
"error": "Refill not found"
}
}
]
Create cancel
| Parameters | Description |
|---|---|
key | Your API key |
action | cancel |
orders | Order IDs (separated by a comma, up to 100 IDs) |
Example response
[
{
"order": 9,
"cancel": {
"error": "Incorrect order ID"
}
},
{
"order": 2,
"cancel": 1
}
]
User balance
| Parameters | Description |
|---|---|
key | Your API key |
action | balance |
Example response
{
"balance": "100.84292",
"currency": "USD"
}
PHP code example
Copy this class into your project and set your API key.
<?php
class Api
{
/** API URL */
public $api_url = 'https://liberlite.com/api/v2';
/** Your API key */
public $api_key = '';
/** Add order */
public function order($data)
{
$post = array_merge(['key' => $this->api_key, 'action' => 'add'], $data);
return json_decode((string)$this->connect($post));
}
/** Get order status */
public function status($order_id)
{
return json_decode($this->connect(['key' => $this->api_key, 'action' => 'status', 'order' => $order_id]));
}
/** Get orders status */
public function multiStatus($order_ids)
{
return json_decode($this->connect(['key' => $this->api_key, 'action' => 'status', 'orders' => implode(',', (array)$order_ids)]));
}
/** Get services */
public function services()
{
return json_decode($this->connect(['key' => $this->api_key, 'action' => 'services']));
}
/** Refill order */
public function refill(int $orderId)
{
return json_decode($this->connect(['key' => $this->api_key, 'action' => 'refill', 'order' => $orderId]));
}
/** Refill orders */
public function multiRefill(array $orderIds)
{
return json_decode($this->connect(['key' => $this->api_key, 'action' => 'refill', 'orders' => implode(',', $orderIds)]), true);
}
/** Get refill status */
public function refillStatus(int $refillId)
{
return json_decode($this->connect(['key' => $this->api_key, 'action' => 'refill_status', 'refill' => $refillId]));
}
/** Get refill statuses */
public function multiRefillStatus(array $refillIds)
{
return json_decode($this->connect(['key' => $this->api_key, 'action' => 'refill_status', 'refills' => implode(',', $refillIds)]), true);
}
/** Cancel orders */
public function cancel(array $orderIds)
{
return json_decode($this->connect(['key' => $this->api_key, 'action' => 'cancel', 'orders' => implode(',', $orderIds)]), true);
}
/** Get balance */
public function balance()
{
return json_decode($this->connect(['key' => $this->api_key, 'action' => 'balance']));
}
private function connect($post)
{
$ch = curl_init($this->api_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; SMM-API-Client)');
$result = curl_exec($ch);
if (curl_errno($ch) != 0 && empty($result)) {
$result = false;
}
curl_close($ch);
return $result;
}
}
// Examples
$api = new Api();
$services = $api->services(); # Return all services
$balance = $api->balance(); # Return user balance
// Add order
$order = $api->order(['service' => 1, 'link' => 'http://example.com/test', 'quantity' => 100, 'runs' => 2, 'interval' => 5]); # Default
$order = $api->order(['service' => 1, 'link' => 'http://example.com/test', 'comments' => "good pic\ngreat photo\n:)\n;)"]); # Custom Comments
$order = $api->order(['service' => 1, 'link' => 'http://example.com/test', 'quantity' => 100, 'hashtag' => "test"]); # Mentions Hashtag
$order = $api->order(['service' => 1, 'link' => 'http://example.com/test']); # Package
$order = $api->order(['service' => 1, 'link' => 'http://example.com/test', 'quantity' => 100, 'runs' => 10, 'interval' => 60]); # Drip-feed
$order = $api->order(['service' => 1, 'link' => 'http://example.com/test', 'quantity' => 100, 'username' => "test"]); # Comment Likes
$order = $api->order(['service' => 1, 'link' => 'http://example.com/test', 'quantity' => 100, 'answer_number' => '7']); # Poll
$status = $api->status($order->order); # Return status, charge, remains, start count, currency
$statuses = $api->multiStatus([1, 2, 3]); # Return orders status, charge, remains, start count, currency
$refill = (array) $api->multiRefill([1, 2]);
$refillIds = array_column($refill, 'refill');
if ($refillIds) {
$refillStatuses = $api->multiRefillStatus($refillIds);
}
Marketplace: licensed scripts
Build a website script that checks a licence key from LiberLite, then sell it in the marketplace. Everything is on this page: how selling works, the licence API, and the code to copy.
This guide explains how to protect a website script with a LiberLite licence key and sell it in the LiberLite marketplace. It is written so you can follow it yourself, or hand it to a developer or an AI assistant (Claude, ChatGPT…) and ask it to make your script work with LiberLite. Section 4 installs a ready-made LiberLite skill into your AI, so it already knows every rule below.
Everything you need is on this page: the requirements, the licence API, and complete code to copy. Use the menu to jump between sections; every code block has a Copy button.
- Licence API base URL:
https://liberlite.com/api/licenses/v1 - LiberLite public key (PEM, shown as text): https://liberlite.com/api/licenses/v1/public-key
- Sell your script: https://liberlite.com/app/sell
What changed in this guide (22 October 2026)
- You choose your slug and reserve it with a draft (no file needed): "Step 1: choose and reserve your slug → develop with PRODUCT_SLUG → upload".
- The zip is checked when you send it for review (slug present, no .env, license.json, installed.lock, dumps, .git/, .claude/ or licence keys).
- New box "What is a product slug?" right below, and slug steps throughout.
- Section 4: install the LiberLite skill into your AI with one command.
- Section 11: new step "Verify your slug" with a ready curl command.
- Section 12: "Create the product and copy its slug" is now step 1; draft slugs and slug changes explained.
- Section 14: troubleshooting table that maps every client message to its cause and fix.
- Section 15: client changelog (now version 1.2.0) and roadmap.
- The client gives clear messages for an unknown slug, a key for another product and a state file that is not writable.
- Seller Test key button on the Sell page; the slug is also in the approval notification.
- Fixed the installer example (
preg_replace('/\D+/', …)), one docblock onmessage(), and R1 now says updating to a newer official client is allowed. - No downloadable files any more: everything is on this page (and in the AI skill).
What is a product slug?
- Definition: the product's short name on LiberLite: 3–60 lowercase letters, digits and single dashes, no dash at the start or end — for example
hotel-booking-pro. - You choose it. On Sell on LiberLite → Sell a product, pick A website script with a licence key: the Product slug field is filled from your title and checked live (it must be free, and not a reserved word such as
admin,api,testorliberlite, nor an example name such asmy-scriptoryour-product-slug). - Reserve it with a draft — no file needed. Press Save draft with only the type, title and slug. The slug is then yours, shown on the card as "Product slug for your script" with a copy button, and the Test key button works. A draft with no file is deleted after 90 days and its slug released (you get an email 7 days before).
- The order is: Step 1: choose and reserve your slug (draft, no file needed) → develop with PRODUCT_SLUG → upload.
- It is locked once the first licence key exists (a sale or your Test key), because installed copies use it. Until then you can still change it on the Sell page.
- Only you, the seller, set it — in one constant (
PRODUCT_SLUG), before you build the zip. Buyers never see or type it; they only enter their licence key. - A wrong slug is the most common integration mistake. Section 11 verifies it in one command, and the zip check warns you if your zip does not contain it.
1. How it works
For the buyer:
- They buy your script in the LiberLite marketplace. They get the download and a licence key such as
LL-7KQ9M-XR2PD-4HJWT-N8CVB. - They upload your script to their hosting and run its installer. The installer:
- asks for the database and tests it;
- asks for the licence key and activates it with LiberLite;
- only then installs.
- While the licence is valid, the website works.
- Suspended, revoked or expired licence: the website shows "not licensed", with the marketplace link and the LiberLite help phone.
- Moving domains: they release the old domain on LiberLite and install on the new one.
For you, the seller:
- Listing: you list the script on LiberLite. Each listing sets how many live domains a key may use and how long the licence lasts. Saving it gives you the product slug your script needs.
- Review: the LiberLite team approves it before it goes on sale.
- Payment: every sale adds your share to your LiberLite balance at once. LiberLite keeps a small commission, shown on the Sell page.
- Licences: you can see every key you sold and the domains using it, and get a free Test key for yourself.
How the check works:
- Your script contains a small client class and LiberLite's public key.
- LiberLite signs every answer with its private key (RSA-2048, SHA-256), and the client only accepts answers whose signature is valid. A fake licence server, or an edited licence file, is therefore rejected.
- The client saves the last signed answer, so normal page loads need no network. It asks LiberLite again about every 12 hours.
- If liberlite.com cannot be reached, the script keeps working for 72 hours after the last valid answer.
2. What you add to your script
Your script keeps its own structure, framework and file names. You add four things, wherever they fit best in your code:
| # | What | Where it goes in your script |
|---|---|---|
| 1 | The client class LiberLiteLicense (section 8), unchanged, LiberLite's public key (a .pem file or a string constant) and your product slug (reserved with a draft) in one constant | Any folder your code can require, e.g. next to your other libraries |
| 2 | A licence step in your installer | Between "database connection tested" and "write the configuration / create the tables" |
| 3 | A licence guard that runs on every request | Your front controller, bootstrap, middleware, base controller or plugin init |
| 4 | Optional: a licence page in your admin area | Your settings or admin section |
The client needs PHP 7.4 or newer, the OpenSSL extension, and either cURL or allow_url_fopen, plus outbound HTTPS to liberlite.com. Scripts in other languages call the same HTTP API (section 6) and check the signature the same way.
3. Integration requirements
These are the exact rules. A script that follows all of them works with LiberLite and passes review.
R1. Ship the client and the public key.
- Include
LiberLiteLicense.phpexactly as given in section 8. Do not edit its logic. - Updating is allowed and recommended: replace the whole file with a newer official version from section 8 whenever one is published (see the changelog in section 15). "Do not edit" means: no changes of your own.
- Include LiberLite's public key from https://liberlite.com/api/licenses/v1/public-key. Save it as a file or paste it into a constant.
- Never ship, request or store LiberLite's private key; you do not need it.
R2. Use your product slug.
- Choose your slug and reserve it with Save draft on the Sell page before you write code, then copy it (see "What is a product slug?" above).
- Every
new LiberLiteLicense([...])call uses'product' => PRODUCT_SLUG, with the real slug, never a placeholder such asyour-product-slugormy-script. - Keep it in one constant, so the installer and the guard always use the same value.
- Verify it with the command in section 11 before you build the zip.
R3. Choose a licence state file.
storage_fileis a JSON file the client writes. It holds this installation's random ID and the last signed answer.- It must be writable by PHP and must not be downloadable from the web: keep it outside the web root, or in a folder blocked with
Deny from all. - It belongs to one installation. Never ship it inside your zip.
- The installer and the guard must use the same file.
R4. Installer order. Your installer must follow this order:
- Ask for the database details and test the connection. Show the database error if it fails.
- Ask for the licence key and call
$license->activate($key). - If
$answer['valid']is nottrue:
- show
$license->message()(it says why and where to get help); - stay on the licence step;
- do not write anything.
- Only when the licence is valid: write your configuration, create your tables, create the admin account and mark the script as installed.
Nothing may be written to disk or to the database before step 4. A wrong key must leave the server unchanged.
R5. Guard every request.
- Before your application handles a request, call
$license->isValid(). This covers web pages, the admin area, AJAX/API endpoints and cron jobs. - If it returns
false: - stop with HTTP 403 and show
$license->message(), with buttons from$license->help()(marketplace link, call, WhatsApp); - cron jobs and CLI commands exit with a non-zero code instead.
- Run the guard after the "is it installed?" check, so a fresh copy still reaches the installer.
- Do not add any setting, environment variable, debug flag or "trial mode" that skips the guard.
R6. Leave the timing to the client.
- Call
isValid()on every request; it only goes online when a check is due. - Do not call the API on every request yourself.
- Do not set
check_everybelow 1 hour orgraceabove 72 hours.
R7. Offer a licence page in your admin area (recommended).
- Show the status, the domain and the expiry date. They come from
$license->details():status,domain,license.expires_at,license.max_activations,license.activations_used. - Add a "Release this domain" button that calls
$license->deactivate()before the owner moves the site. - Never show the full licence key on public pages. In the admin area, mask all but the last 5 characters.
R8. Keep the zip clean. Your download must not contain:
- your configuration file;
- the licence state file;
- the "installed" marker;
- test data;
- a licence key (including your Test key).
R9. Test installs are free.
- Installs on
localhost,127.0.0.1,*.test,*.localand*.localhostdo not use a live-domain slot. - Do not add special code for them.
4. Using an AI assistant to integrate your script
Install the LiberLite skill (recommended). A skill is a small instruction pack your AI loads when it works on your code. The LiberLite skill contains every rule of this guide, the client class, the example installer and guard, and the slug check — so the AI follows them without you pasting anything.
Run one command in your script's folder:
curl -fsSL https://liberlite.com/ai/install.sh | sh
On Windows (PowerShell):
irm https://liberlite.com/ai/install.ps1 | iex
- It creates
.claude/skills/liberlite-license/in the current folder (for Claude Code and other tools that read skills from there). To install it for all your projects instead, add--user(curl -fsSL https://liberlite.com/ai/install.sh | sh -s -- --user) or, in PowerShell, run$env:LIBERLITE_SKILL_SCOPE='user'first. It then goes to~/.claude/skills/liberlite-license/. - Run the same command again to update the skill.
- For an AI chat that cannot install skills (ChatGPT, Gemini…), open https://liberlite.com/ai/skills/liberlite-license/SKILL.md and paste its text into the chat.
Then ask your AI, for example: "Add the LiberLite licence to this script. My product slug is my-booking-script."
Or copy this prompt and fill in the three values in capitals:
I sell my PHP script on the LiberLite marketplace. Make it check a LiberLite licence key,
following the LiberLite Developer Guide exactly:
https://liberlite.com/api/docs#licensed-scripts
My product slug is: YOUR-PRODUCT-SLUG (from Sell on LiberLite → my product → "Product slug")
My script's version is: YOUR-VERSION
My script is built with: PLAIN PHP / LARAVEL / CODEIGNITER / WORDPRESS PLUGIN / OTHER
Do this:
1. Add the LiberLiteLicense class from section 8 unchanged, and LiberLite's public key.
Put them where libraries live in my project.
2. Put my product slug in one constant PRODUCT_SLUG (rule R2). Never use a placeholder.
3. Pick a licence state file that is writable and not reachable from the web (rule R3).
4. Change my installer so the order is:
database details → test connection → licence key (activate) → only then write config,
create tables and mark as installed (rule R4).
If I have no installer, create one that follows the example in section 9.
5. Add the licence guard on every request (web, admin, API, cron) after the "installed?"
check (rule R5). Use the idiomatic place for my framework (section 5).
6. Add an admin licence page with status, expiry and a "Release this domain" button (rule R7).
7. Make sure the zip does not contain config, licence state or installed marker files (rule R8).
8. Do not add any way to skip the licence check.
Then list every file you changed and tell me how to test it, including the slug check in section 11.
5. Where the guard goes in common frameworks
These are starting points. Every example assumes a helper that returns one configured client:
const PRODUCT_SLUG = 'my-booking-script'; // YOUR real slug: Sell on LiberLite → your product → "Product slug"
function liberlite_license(): LiberLiteLicense
{
static $license = null;
return $license ??= new LiberLiteLicense([
'product' => PRODUCT_SLUG,
'public_key' => (string)file_get_contents('/path/to/liberlite-public.pem'),
'storage_file' => '/path/outside/web/root/license.json',
'version' => '1.0.0', // your script's version
]);
}
Plain PHP (one front controller): put this at the top of index.php, after loading your config:
if (!is_installed()) { // your own check, e.g. config file exists
header('Location: /install.php');
exit;
}
if (!liberlite_license()->isValid()) {
http_response_code(403);
exit('This copy is not licensed: ' . htmlspecialchars(liberlite_license()->message()));
}
Plain PHP with many entry files: put the same check in the common file that every page includes, such as header.php, init.php or bootstrap.php.
Laravel: add a middleware and register it globally (in bootstrap/app.php on Laravel 11+, or in the web and api groups of app/Http/Kernel.php on older versions). Skip the installer routes.
class CheckLiberLiteLicense
{
public function handle($request, \Closure $next)
{
if ($request->is('install*')) {
return $next($request);
}
if (!liberlite_license()->isValid()) {
abort(403, liberlite_license()->message());
}
return $next($request);
}
}
CodeIgniter 4: create a filter whose before() method runs the same check and returns a 403 response. Then add it to $globals['before'] in app/Config/Filters.php, excluding the installer routes.
WordPress plugin or theme:
- Licence step: ask for the key on your settings page and call
activate()there. Store the state file in the uploads folder with a random name, or in a folder blocked from the web. - Guard: hook
plugins_loaded(orafter_setup_theme). WhenisValid()is false, disable your plugin's features and show an admin notice withmessage(). Do not break the whole WordPress site.
Cron jobs and CLI commands: check at the start and exit with code 1 when the licence is not valid.
6. Licence API reference
Base URL: https://liberlite.com/api/licenses/v1.
- Requests:
POST, sent as form fields or JSON. - Rate limit: 300 requests per hour per IP.
- The client class in section 8 does all of this for you. You only need this section for other languages, or to understand the answers.
| Endpoint | Purpose |
|---|---|
POST /activate | Activate this installation. Uses a live-domain slot; activating the same domain again is free. |
POST /check | Periodic check of an activated installation. |
POST /deactivate | Release this installation's slot (before moving to another domain). |
GET /public-key | LiberLite's public key (PEM). |
Request fields:
| Field | Required | Notes |
|---|---|---|
license_key | yes | LL-XXXXX-XXXXX-XXXXX-XXXXX; spaces and letter case are ignored |
product | yes | your product slug (or id); a key only works for its own product |
domain | yes (not for deactivate) | host name only, e.g. shop.co.tz; www. is ignored |
instance_id | yes | 16–64 letters/digits, random, created once per installation and kept in the state file |
version | no | your script's version (shown to you in Licences) |
php | no | PHP version (shown to you in Licences) |
The order of the checks: LiberLite first looks up the product slug, then the key. So a wrong slug always answers unknown_product, whatever key you send — that is what makes the slug check in section 11 work.
Answer (HTTP 200):
{
"payload": "{\"valid\":true,\"status\":\"active\",…}",
"signature": "base64…",
"license": {
"valid": true, "status": "active", "message": "Licence active.", "action": "activate",
"domain": "shop.co.tz", "instance_id": "…",
"product": {"id": 3, "slug": "my-booking-script", "title": "My Booking Script"},
"license": {"expires_at": null, "max_activations": 1, "activations_used": 1},
"checked_at": "2026-10-22T10:00:00+00:00", "next_check_seconds": 43200
}
}
Trust only payload + signature. The license object is the same data, decoded for convenience. Before trusting an answer, a client must:
- Verify the signature. In PHP:
openssl_verify($payload, base64_decode($signature), $publicKey, OPENSSL_ALGO_SHA256) === 1. - Check it belongs to this installation. Decode
payloadand confirm thatinstance_idis yours andproduct.slugis your product. When the slug is unknown,productisnullandstatusisunknown_product. - Read the result. Treat the licence as valid only when
validistrue.
status values:
| Status | Meaning | What your script shows |
|---|---|---|
active | valid: true, everything fine | nothing, the site works |
invalid | the slug is fine, but the key does not exist | "Licence key not found" |
wrong_product | the key belongs to another product | "This key is for another product" |
unknown_product | no licensed script has this slug | the seller must fix PRODUCT_SLUG |
suspended / revoked | stopped by you or LiberLite | "not licensed" page |
expired | the licence length has passed | "not licensed" page, renew on LiberLite |
limit_reached | all live-domain slots are used (the message names the domains) | ask the owner to release a domain |
not_activated | check was called before activate | send the owner to the licence step |
released | answer to deactivate | slot freed |
The client puts LiberLite's own text in message(), so you can show it as it is.
Every refused answer says where to get help. When valid is false:
messageends with a help line, for example: "This licence key belongs to another product. Need help? Visit https://liberlite.com/app/marketplace or contact +255753099003."helpholds the same parts separately, so you can build buttons:{"text": "Need help? …", "marketplace": "https://liberlite.com/app/marketplace", "phone": "+255753099003"}.
The client also adds this help line to its own messages, and help() returns the parts. Show them on your "not licensed" page and in your installer, as the examples in sections 9 and 10 do.
Errors: missing or invalid fields return HTTP 422 {"error": {"code": "...", "message": "..."}}. These answers are not signed.
Other languages use RSA PKCS#1 v1.5 with SHA-256:
- Node.js:
crypto.verify('sha256', Buffer.from(payload), publicKeyPem, Buffer.from(signature, 'base64')) - Python (
cryptography):load_pem_public_key(pem).verify(base64.b64decode(sig), payload.encode(), padding.PKCS1v15(), hashes.SHA256())
7. Security: what the licence protects, and how to harden it
What it protects:
- No fake answers. A copied or edited state file, a fake licence server or a key for another product does not work.
- Domain limit. The limit is counted on liberlite.com, not in your code.
What no PHP licence can do on its own: stop someone who edits your PHP files and deletes the check — or changes PRODUCT_SLUG to the slug of a cheaper script they bought. Make that hard:
- Encode your files. Encode
LiberLiteLicense.phpand your core files with ionCube or SourceGuardian before zipping. Then say "requires the ionCube loader" in your listing. - Check in several places. Besides the guard, call
isValid()before important actions: exports, payments, sending emails. - Keep valuable parts on a server you control. For example, an API your script calls with the licence key. A cracked copy then misses those features.
8. The client class: LiberLiteLicense.php (copy unchanged)
Current version: 1.2.0 (LiberLiteLicense::VERSION). See the changelog in section 15.
| Method | Use |
|---|---|
new LiberLiteLicense([...]) | Required: product (your slug), public_key (PEM text) and storage_file (writable, not public). Optional: version, api, check_every (12 h), grace (72 h), domain (default: current host). |
activate($key) | In the installer or licence page. Returns the answer: valid, status, message, license.*. |
isValid() | On every request: true or false. Goes online only when a check is due. |
message() | The reason to show when the licence is not valid, ending with where to get help. |
help() | Where to get help: ['text' => …, 'marketplace' => url, 'phone' => '+255…'], for buttons on your "not licensed" page. |
details() | The last verified answer (status, domain, expiry, activations). |
deactivate() | Release this installation's domain slot. |
instanceId() | This installation's random ID (stored in the state file). |
<?php
declare(strict_types=1);
/**
* LiberLite licence client — drop this one file into any PHP script you sell on the LiberLite marketplace.
* No dependencies (uses cURL or PHP streams, and OpenSSL). PHP 7.4+.
*
* Version 1.2.0 (see LiberLiteLicense::VERSION). Changelog:
* 1.2.0 Clear messages for an unknown product slug, a key for another product and a state file that is not
* writable (these used to say "meant for another installation"); VERSION constant.
* 1.1.0 help(): every refused licence says where to get help (marketplace link and phone).
* 1.0.0 First release.
* Replace this file with a newer official version at any time; never edit its logic.
*
* $lic = new LiberLiteLicense([
* 'product' => 'my-booking-script', // your product slug: Sell on LiberLite → your product → "Product slug"
* 'public_key' => file_get_contents(__DIR__ . '/liberlite-public.pem'), // from /api/licenses/v1/public-key
* 'storage_file' => __DIR__ . '/../storage/license.json', // writable, NOT web-accessible
* 'version' => '1.0.0',
* ]);
* $r = $lic->activate($keyTheBuyerTyped); // in your installer
* if (!$lic->isValid()) { exit($lic->message()); } // in your app's bootstrap
*
* Every answer from LiberLite is signed (RSA-SHA256). This client only trusts answers whose signature verifies with
* LiberLite's public key, so a fake server or an edited licence file cannot unlock the script.
* Guide: https://liberlite.com/api/docs#licensed-scripts
*/
final class LiberLiteLicense
{
public const VERSION = '1.2.0';
/** @var array<string,mixed> */
private array $cfg;
/** @var array<string,mixed> */
private array $state = [];
private string $message = '';
public function __construct(array $config)
{
$this->cfg = $config + [
'api' => 'https://liberlite.com/api/licenses/v1',
'version' => '1.0.0',
'check_every' => 12 * 3600, // seconds between online checks (LiberLite may ask for another interval)
'grace' => 72 * 3600, // keep working this long when liberlite.com cannot be reached
'timeout' => 15,
'domain' => null, // default: the current host name
// Added to every "not licensed" message so the site owner knows where to get help.
'help' => 'Need help? Visit https://liberlite.com/app/marketplace or contact +255753099003.',
'help_phone' => '+255753099003',
'help_url' => 'https://liberlite.com/app/marketplace',
];
foreach (['product', 'public_key', 'storage_file'] as $k) {
if (empty($this->cfg[$k])) {
throw new InvalidArgumentException("LiberLiteLicense: '$k' is required.");
}
}
$this->load();
}
// ------------------------------------------------------------------ public API
/** Activate a licence key on this installation (call it from your installer). Returns the licence answer. */
public function activate(string $licenseKey): array
{
$licenseKey = strtoupper(preg_replace('/\s+/', '', $licenseKey));
$r = $this->call('activate', $licenseKey);
if ($r !== null && !empty($r['valid'])) {
$this->state['license_key'] = $licenseKey;
$this->save();
}
return $r ?? ['valid' => false, 'status' => 'unreachable', 'message' => $this->message];
}
/**
* Is this installation licensed? Checks online when a check is due; between checks (and for up to 'grace'
* seconds while LiberLite cannot be reached) the last signed answer is used. Cheap enough for every request.
*/
public function isValid(): bool
{
$last = $this->verifiedAnswer();
if (!$last || empty($this->state['license_key'])) {
$this->message = $this->withHelp('This copy is not activated. Run the installer and enter your licence key.');
return false;
}
$every = (int)($last['next_check_seconds'] ?? $this->cfg['check_every']);
$age = time() - (int)($this->state['checked_at'] ?? 0);
if ($age >= max(300, $every)) {
$fresh = $this->call('check', (string)$this->state['license_key']);
if ($fresh !== null) {
return !empty($fresh['valid']) || $this->fail($fresh);
}
// LiberLite unreachable: keep working within the grace period after the last good answer.
if (!empty($last['valid']) && time() - (int)($this->state['ok_at'] ?? 0) < (int)$this->cfg['grace']) {
return true;
}
$this->message = $this->withHelp('The licence could not be checked with LiberLite for a while. Check this server\'s internet connection.');
return false;
}
return !empty($last['valid']) || $this->fail($last);
}
/** Release this installation's activation (e.g. before moving to another domain). */
public function deactivate(): bool
{
if (empty($this->state['license_key'])) {
return true;
}
$r = $this->call('deactivate', (string)$this->state['license_key']);
if ($r !== null) {
unset($this->state['license_key'], $this->state['payload'], $this->state['signature']);
$this->save();
return true;
}
return false;
}
/** Why the licence is not valid (show it to the site owner), always ending with where to get help. */
public function message(): string
{
return $this->message;
}
/**
* Where to get help when the licence is refused: ['text' => …, 'marketplace' => url, 'phone' => '+255…'].
* Comes from LiberLite's last answer when there is one, else from the 'help…' settings.
*/
public function help(): array
{
$h = $this->verifiedAnswer()['help'] ?? null;
return is_array($h) && !empty($h['text']) ? $h + ['marketplace' => $this->cfg['help_url'], 'phone' => $this->cfg['help_phone']]
: ['text' => (string)$this->cfg['help'], 'marketplace' => (string)$this->cfg['help_url'], 'phone' => (string)$this->cfg['help_phone']];
}
/** The last verified licence answer (status, license.expires_at, activations…) or null. */
public function details(): ?array
{
return $this->verifiedAnswer();
}
public function instanceId(): string
{
if (empty($this->state['instance_id'])) {
$this->state['instance_id'] = bin2hex(random_bytes(16));
$this->save();
}
return (string)$this->state['instance_id'];
}
// ------------------------------------------------------------------ internals
private function domain(): string
{
$d = (string)($this->cfg['domain'] ?? '');
if ($d === '') {
$d = (string)($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost');
}
$d = strtolower(preg_replace('/:\d+$/', '', $d));
return strpos($d, 'www.') === 0 ? substr($d, 4) : $d;
}
/** Calls LiberLite; returns the verified answer, or null when unreachable / not verifiable ($this->message says why). */
private function call(string $action, string $key): ?array
{
$fields = ['license_key' => $key, 'product' => $this->cfg['product'], 'domain' => $this->domain(), 'instance_id' => $this->instanceId(),
'version' => $this->cfg['version'], 'php' => PHP_VERSION];
$body = $this->post(rtrim((string)$this->cfg['api'], '/') . '/' . $action, $fields);
$j = $body === null ? null : json_decode($body, true);
if (!is_array($j)) {
$this->message = $this->withHelp('Could not reach LiberLite to check the licence. Try again in a moment.');
return null;
}
if (isset($j['error'])) { // validation problems (missing domain…) are not signed
$this->message = $this->withHelp((string)($j['error']['message'] ?? 'LiberLite refused the request.'));
return null;
}
$payload = (string)($j['payload'] ?? '');
$answer = $this->verify($payload, (string)($j['signature'] ?? ''));
if ($answer === null) {
$this->message = $this->withHelp('The licence answer is not signed by LiberLite. Is the public key correct?');
return null;
}
if (($answer['instance_id'] ?? '') !== $this->instanceId()) {
// Usually the state file could not be saved, so a new installation ID is made on every request.
$this->message = $this->withHelp($this->stateWritable()
? 'The licence answer was meant for another installation.'
: 'The licence state file ' . $this->cfg['storage_file'] . ' is not writable, so this installation cannot keep its ID. Make it writable by PHP.');
return null;
}
if (($answer['status'] ?? '') === 'unknown_product') {
$this->message = $this->withHelp("Unknown product slug '" . $this->cfg['product'] . "' — check PRODUCT_SLUG in this script. The seller copies it from Sell on LiberLite → the product → Product slug.");
return null;
}
if (($answer['product']['slug'] ?? '') !== $this->cfg['product'] && (string)($answer['product']['id'] ?? '') !== (string)$this->cfg['product']) {
$this->message = $this->withHelp("LiberLite answered for product '" . ($answer['product']['slug'] ?? '?') . "', but this script is set to '" . $this->cfg['product'] . "'. Check PRODUCT_SLUG.");
return null;
}
$this->state['payload'] = $payload;
$this->state['signature'] = (string)$j['signature'];
$this->state['checked_at'] = time();
if (!empty($answer['valid'])) {
$this->state['ok_at'] = time();
}
$this->save();
$this->message = empty($answer['valid']) ? $this->withHelp((string)($answer['message'] ?? 'This licence is not valid.')) : (string)($answer['message'] ?? '');
return $answer;
}
private function verifiedAnswer(): ?array
{
return empty($this->state['payload']) ? null : $this->verify((string)$this->state['payload'], (string)($this->state['signature'] ?? ''));
}
private function verify(string $payload, string $signatureB64): ?array
{
$sig = base64_decode($signatureB64, true);
if ($payload === '' || $sig === false || openssl_verify($payload, $sig, (string)$this->cfg['public_key'], OPENSSL_ALGO_SHA256) !== 1) {
return null;
}
$a = json_decode($payload, true);
return is_array($a) ? $a : null;
}
private function fail(array $answer): bool
{
$this->message = $this->withHelp((string)($answer['message'] ?? 'This licence is not valid.'));
return false;
}
/** Adds the help line once (LiberLite's own answers already carry it). */
private function withHelp(string $message): string
{
return str_contains($message, 'Need help?') ? $message : rtrim($message) . ' ' . $this->cfg['help'];
}
private function post(string $url, array $fields): ?string
{
$data = http_build_query($fields);
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $data, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => (int)$this->cfg['timeout'],
CURLOPT_CONNECTTIMEOUT => 8, CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/x-www-form-urlencoded']]);
$out = curl_exec($ch);
curl_close($ch);
return is_string($out) && $out !== '' ? $out : null;
}
$ctx = stream_context_create(['http' => ['method' => 'POST', 'header' => "Content-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\n", 'content' => $data, 'timeout' => (int)$this->cfg['timeout'], 'ignore_errors' => true]]);
$out = @file_get_contents($url, false, $ctx);
return is_string($out) && $out !== '' ? $out : null;
}
private function stateWritable(): bool
{
$f = (string)$this->cfg['storage_file'];
return is_file($f) ? is_writable($f) : is_dir(dirname($f)) && is_writable(dirname($f));
}
private function load(): void
{
$f = (string)$this->cfg['storage_file'];
$j = is_file($f) ? json_decode((string)file_get_contents($f), true) : null;
$this->state = is_array($j) ? $j : [];
}
private function save(): void
{
$f = (string)$this->cfg['storage_file'];
if (!is_dir(dirname($f))) {
@mkdir(dirname($f), 0750, true);
}
@file_put_contents($f, json_encode($this->state, JSON_PRETTY_PRINT), LOCK_EX);
}
}
9. Example installer (adapt it to your script)
This plain-PHP installer shows the required order (rule R4). Your installer can look completely different. Keep the order, and use your own paths, schema and configuration format. Set PRODUCT_SLUG to your real slug.
<?php
declare(strict_types=1);
/**
* Example installer for a script sold on the LiberLite marketplace. Plain PHP, no framework.
*
* Step 1 database details → the connection is tested, nothing is written yet
* Step 2 licence key → activated with LiberLite (signed answer)
* Step 3 install → only now the configuration is written and the tables are created
*
* Adapt it to your script: your own installer can look completely different, as long as it keeps this order.
* Every path below is an example — point them at wherever your script keeps these files.
*/
const APP_NAME = 'My Script';
const PRODUCT_SLUG = 'my-script'; // your product slug, shown on LiberLite → Sell on LiberLite
const APP_VERSION = '1.0.0';
const LICENSE_CLIENT = __DIR__ . '/LiberLiteLicense.php'; // the client class from the guide
const PUBLIC_KEY_FILE = __DIR__ . '/liberlite-public.pem'; // LiberLite's public key (ship it with your script)
const LICENSE_STATE_FILE = __DIR__ . '/license.json'; // writable; must NOT be downloadable from the web
const CONFIG_FILE = __DIR__ . '/config.php'; // your script's configuration, written at the end
const LOCK_FILE = __DIR__ . '/installed.lock'; // marks the script as installed
const SCHEMA_FILE = __DIR__ . '/schema.sql'; // your tables
require LICENSE_CLIENT;
if (is_file(LOCK_FILE)) {
http_response_code(404);
exit('Already installed.');
}
session_start();
$_SESSION['csrf'] ??= bin2hex(random_bytes(16));
$license = new LiberLiteLicense([
'product' => PRODUCT_SLUG,
'public_key' => (string)file_get_contents(PUBLIC_KEY_FILE),
'storage_file' => LICENSE_STATE_FILE,
'version' => APP_VERSION,
]);
$error = null;
$licenseHelp = null;
$step = isset($_SESSION['db']) ? 2 : 1;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!hash_equals($_SESSION['csrf'], (string)($_POST['csrf'] ?? ''))) {
$error = 'Session expired, please try again.';
} elseif (($_POST['step'] ?? '') === '1') {
// ---------------- Step 1: test the database connection (nothing is saved to disk yet)
$db = ['host' => trim($_POST['host'] ?? 'localhost'), 'port' => (int)($_POST['port'] ?? 3306) ?: 3306, 'name' => trim($_POST['name'] ?? ''), 'user' => trim($_POST['user'] ?? ''), 'pass' => (string)($_POST['pass'] ?? '')];
try {
new PDO("mysql:host={$db['host']};port={$db['port']};dbname={$db['name']};charset=utf8mb4", $db['user'], $db['pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 8]);
$_SESSION['db'] = $db;
$step = 2;
} catch (Throwable $e) {
$error = 'Could not connect to the database: ' . $e->getMessage();
}
} elseif (($_POST['step'] ?? '') === '2' && isset($_SESSION['db'])) {
// ---------------- Step 2: activate the licence key with LiberLite
$answer = $license->activate((string)($_POST['license_key'] ?? ''));
if (empty($answer['valid'])) {
$error = $license->message(); // LiberLite's reason + where to get help (marketplace link, phone)
$licenseHelp = $license->help();
} else {
// ---------------- Step 3: only a licensed copy writes its configuration and creates its tables
$db = $_SESSION['db'];
$pdo = new PDO("mysql:host={$db['host']};port={$db['port']};dbname={$db['name']};charset=utf8mb4", $db['user'], $db['pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
foreach (array_filter(array_map('trim', explode(";\n", (string)file_get_contents(SCHEMA_FILE)))) as $sql) {
$pdo->exec($sql);
}
file_put_contents(CONFIG_FILE, "<?php\nreturn " . var_export(['db' => $db, 'installed_at' => gmdate('c')], true) . ";\n");
@mkdir(dirname(LOCK_FILE), 0750, true);
file_put_contents(LOCK_FILE, gmdate('c'));
unset($_SESSION['db']);
$step = 3;
}
}
}
$e = fn($s) => htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
?><!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Install <?= $e(APP_NAME) ?></title>
<style>body{font:16px/1.5 system-ui,sans-serif;max-width:560px;margin:40px auto;padding:0 16px}input{width:100%;padding:10px;margin:4px 0 12px;box-sizing:border-box}button{padding:10px 18px}.err{background:#fde8ec;color:#a1102a;padding:10px;border-radius:8px}</style></head>
<body><h1>Install <?= $e(APP_NAME) ?></h1>
<?php if ($error): ?><div class="err" role="alert"><p style="margin:0"><?= $e($error) ?></p>
<?php if (!empty($licenseHelp)): $digits = preg_replace('/\D+/', '', (string)$licenseHelp['phone']); ?><p style="margin:10px 0 0"><a href="<?= $e($licenseHelp['marketplace']) ?>" target="_blank" rel="noopener">Open LiberLite marketplace</a><?php if ($digits): ?> · <a href="tel:+<?= $e($digits) ?>">Call <?= $e($licenseHelp['phone']) ?></a> · <a href="https://wa.me/<?= $e($digits) ?>" target="_blank" rel="noopener">WhatsApp</a><?php endif; ?></p><?php endif; ?></div><?php endif; ?>
<?php if ($step === 1): ?>
<h2>1. Database</h2>
<form method="post"><input type="hidden" name="csrf" value="<?= $e($_SESSION['csrf']) ?>"><input type="hidden" name="step" value="1">
<label>Host <input name="host" value="localhost"></label><label>Port <input name="port" value="3306"></label><label>Database name <input name="name" required></label>
<label>User <input name="user" required></label><label>Password <input name="pass" type="password"></label>
<button>Test connection</button></form>
<?php elseif ($step === 2): ?>
<h2>2. Licence key</h2>
<p>Database connection OK. Enter the licence key you got when you bought <?= $e(APP_NAME) ?> on LiberLite (Marketplace → your purchase).</p>
<form method="post"><input type="hidden" name="csrf" value="<?= $e($_SESSION['csrf']) ?>"><input type="hidden" name="step" value="2">
<label>Licence key <input name="license_key" placeholder="LL-XXXXX-XXXXX-XXXXX-XXXXX" required autocomplete="off"></label>
<button>Activate and install</button></form>
<?php else: ?>
<h2>3. Done</h2><p>Your licence is active and <?= $e(APP_NAME) ?> is installed. <a href="/">Open the website</a>.</p>
<?php endif; ?>
</body></html>
10. Example guard
<?php
declare(strict_types=1);
/**
* Licence guard for a script sold on the LiberLite marketplace. Load it early on every request — in your front
* controller, bootstrap, middleware or base controller, whatever your script uses — after checking that the
* installer has run. Valid licence: nothing happens. Otherwise the visitor gets a short "not licensed" page (HTTP 403).
* The online check runs at most every 12 hours; in between, the last signed answer is used (no network needed).
*
* The paths are examples: use the same files as your installer.
*/
require_once __DIR__ . '/LiberLiteLicense.php';
$liberliteLicense = new LiberLiteLicense([
'product' => 'my-script', // your product slug
'public_key' => (string)file_get_contents(__DIR__ . '/liberlite-public.pem'),
'storage_file' => __DIR__ . '/license.json', // the same file the installer used
'version' => '1.0.0',
]);
if (!$liberliteLicense->isValid()) {
$h = $liberliteLicense->help();
$esc = fn($v) => htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
$digits = preg_replace('/\D+/', '', (string)($h['phone'] ?? ''));
http_response_code(403);
header('Content-Type: text/html; charset=utf-8');
echo '<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Licence required</title>'
. '<body style="font:16px/1.5 system-ui,sans-serif;max-width:560px;margin:60px auto;padding:0 16px;color:#1f2937">'
. '<div role="alert" style="border:1px solid #fecaca;background:#fef2f2;border-radius:14px;padding:20px">'
. '<h1 style="margin:0 0 8px;font-size:1.3rem;color:#b91c1c">This copy is not licensed</h1>'
. '<p style="margin:0 0 16px">' . $esc($liberliteLicense->message()) . '</p>'
. '<p style="margin:0;display:flex;gap:8px;flex-wrap:wrap">'
. '<a href="' . $esc($h['marketplace']) . '" style="background:#7c3aed;color:#fff;padding:10px 16px;border-radius:10px;text-decoration:none">Open LiberLite marketplace</a>'
. ($digits ? '<a href="tel:+' . $esc($digits) . '" style="border:1px solid #d1d5db;padding:10px 16px;border-radius:10px;text-decoration:none;color:#1f2937">Call ' . $esc($h['phone']) . '</a>'
. '<a href="https://wa.me/' . $esc($digits) . '" style="border:1px solid #d1d5db;padding:10px 16px;border-radius:10px;text-decoration:none;color:#1f2937">WhatsApp</a>' : '')
. '</p></div></body>';
exit;
}
11. Testing your integration
Get a key for testing: on the Sell page, press Test key on your product. It is free, works on localhost and on one live domain, and works before the product is approved. Never ship it in the zip.
Step 0 — Verify your slug (one command, no installation needed). Replace YOUR-SLUG:
curl -X POST https://liberlite.com/api/licenses/v1/activate \
-d "license_key=LL-AAAAA-BBBBB-CCCCC-DDDDD&product=YOUR-SLUG&domain=test.test&instance_id=0123456789abcdef"
Read "status" in the answer:
| Status | Meaning |
|---|---|
unknown_product | The slug is wrong (a placeholder, a typo, or the product was never created). Copy it again from the Sell page. |
invalid | The slug is right. The key in the command is a dummy, so "key does not exist" is expected. |
The dummy key never activates anything and test.test is a local domain, so this check uses no slot.
Then check each of these with your real installation:
- Fresh install on localhost: a wrong key shows LiberLite's message and nothing is written. The right key installs, and the site works.
- Fresh install on a real domain: it uses one live-domain slot. A second domain with the same key shows
limit_reachedwhen the licence allows only one. - Suspend the licence (ask LiberLite support), then delete the state file or wait for the next check. The site must show the "not licensed" page, including admin and API URLs.
- Release the domain from your admin licence page (or from the buyer's marketplace page). Installing on another domain then works.
- Edit the state file (change
"valid":falseto"valid":true). The site must refuse it, because the signature no longer matches. - Zip check: unzip your download into an empty folder and confirm there is no configuration, state file, installed marker or licence key.
12. Selling it on LiberLite
- Choose and reserve your slug (draft, no file needed). Open https://liberlite.com/app/sell, press Sell a product, choose A website script with a licence key, type the title, check the Product slug (filled from the title, checked live) and press Save draft. The card shows "Product slug for your script" with a copy button.
- The slug works with
/activateimmediately — in a draft and while the product waits for review — so you can build and test before approval. - You can still change it until the first licence key exists (a sale or your Test key); then it is locked, and it never changes again.
- A draft with no file is deleted after 90 days and the slug released; you get an email 7 days before.
- Integrate and test. Put the slug in
PRODUCT_SLUG(rule R2), add the licence check (sections 3–10), verify the slug and test with your Test key (section 11). - Complete the listing:
- title and category;
- price, in your currency;
- a clear description: what it does, the requirements (PHP version, MySQL, ionCube), and what the buyer gets;
- live domains per licence;
- licence length (0 = lifetime);
- your support contact.
- Upload the final zip (or give a download link), plus a product image, and press Send for review. The zip is checked first:
- blocked (fix and upload again):
.envfiles,license.json,installed.lock, or a real LiberLite licence key inside your files; - warnings (shown to you and to the reviewer): your slug does not appear in the zip ("Your zip does not contain your product slug — set PRODUCT_SLUG before uploading"),
.sql/.sql.gzfiles,.git/or.claude/folders, or text that looks like a licence key. A download link cannot be checked, so check it yourself against the same list.
- Review. The LiberLite team reviews the listing, usually within a day. If changes are needed, you get a note explaining them. Every edit is reviewed again.
- On sale. When it is approved, you get a notification with the slug, and the product is on sale. Each sale adds your share to your balance, and the Licences button lists every key and its domains.
13. Checklist before you submit
- ☐ The slug is reserved (a draft is enough), and
PRODUCT_SLUGholds it — not a placeholder - ☐ The slug check in section 11 answers
invalid, notunknown_product - ☐
LiberLiteLicense.phpis the latest official version, unchanged, and the public key is LiberLite's - ☐ The state file is writable, not web-accessible, and the same for installer and guard
- ☐ Installer order: database → test → licence → only then write (a wrong key writes nothing)
- ☐ The guard runs on every request type (web, admin, API, cron), after the "installed?" check
- ☐ No setting or flag skips the licence check
- ☐ The admin licence page shows status and expiry, and can release the domain
- ☐ The zip contains no configuration,
.env, state file, installed marker, database dump,.git/,.claude/or licence key - ☐ The listing states requirements and a support contact
14. FAQ and troubleshooting
Troubleshooting: what each message means
| Message the site owner sees | Cause | Fix |
|---|---|---|
"Unknown product slug 'X' — check PRODUCT_SLUG" (client 1.2.0+) or status unknown_product | The slug in the script does not exist on LiberLite: a placeholder, a typo, or the product was never created | Copy the slug from Sell on LiberLite → your product, put it in PRODUCT_SLUG, verify it (section 11) |
| "The licence answer was meant for another installation" (client 1.0–1.1) | Usually the same as above: the slug in the script does not exist or does not match the key's product, so the server answered with a different or empty product. Less often the state file is not writable, so the installation ID changes on every request | Fix PRODUCT_SLUG as above; make the state file writable; update to client 1.2.0, which names the real cause |
| "LiberLite answered for product 'Y', but this script is set to 'X'" | The answer is for another product than PRODUCT_SLUG | Use the slug of this product; make sure every LiberLiteLicense uses the same constant |
| "The licence state file … is not writable" | PHP cannot write the state file or its folder | Give PHP write permission on that file/folder (and keep it out of the web root) |
| "The licence answer is not signed by LiberLite. Is the public key correct?" | Wrong, damaged or outdated public key file | Copy the key again from https://liberlite.com/api/licenses/v1/public-key |
| "Could not reach LiberLite to check the licence" | Outbound HTTPS to liberlite.com is blocked, or the server has neither cURL nor allow_url_fopen | Ask the host to allow outbound HTTPS and enable cURL |
"This licence key does not exist" (status invalid) | The key is mistyped or not a LiberLite key | Copy the key from LiberLite → Marketplace → the purchase |
"This licence key belongs to another product" (status wrong_product) | The buyer used a key bought for another script | Use the key that came with this script |
"…already used on N domain(s)…" (status limit_reached) | All live-domain slots are used | Release a domain on the marketplace page or the admin licence page, or buy another licence |
"This installation is not activated" (status not_activated) | check ran before activate, e.g. the state file was copied from another site | Run the installer's licence step again |
| "This copy is not activated" | No licence was activated on this installation yet | Run the installer and enter the licence key |
Every one of these messages ends with "Need help? Visit https://liberlite.com/app/marketplace or contact +255753099003."
Can I sell documents or videos instead? Yes. Choose "A document or file" or "A video or course" on the Sell page. Buyers download or watch it; there is no licence key.
How does a customer move to a new domain? They press Release on the old domain, on their marketplace page or on your admin licence page. Then they install on the new domain. LiberLite staff can also reset domains.
What happens if the script cannot reach LiberLite? It keeps working for 72 hours after the last valid answer, then shows "not licensed" until it can check again.
Can the buyer use the key on two sites? Only on as many live domains as you allowed. Local test installs never count.
My script is not PHP. Use the API in section 6. Verify the signature with LiberLite's public key and store the signed answer, just like the PHP client does.
Where is my money? Each sale adds your share to your LiberLite balance at once. Use it for LiberLite services, or ask LiberLite support about withdrawals.
15. Changelog and roadmap
LiberLiteLicense.php changelog (the version is in the file header and in LiberLiteLicense::VERSION):
| Version | Changes |
|---|---|
| 1.2.0 | Clear messages instead of "meant for another installation": unknown product slug, an answer for another product, a state file that is not writable. VERSION constant and changelog in the file header. |
| 1.1.0 | help(); every refused licence says where to get help (marketplace link and phone). |
| 1.0.0 | First release. |
Updating is always safe: replace the whole file with the newest version from section 8 (rule R1).
Roadmap and requests
| Request | Status |
|---|---|
| When the slug is unknown, say "Unknown product slug 'X' — check PRODUCT_SLUG" instead of "meant for another installation" | Done in client 1.2.0 |
| Show the slug prominently, with a copy button, on the Sell page and in seller notifications | Done: Sell page card and Test key window (copy buttons), approval notification |
| A seller-only Test key for each product, so sellers can test without a second account | Done: Test key button on the Sell page |
A changelog and version number for LiberLiteLicense.php | Done: header, VERSION, table above |
| An AI skill that knows these rules | Done: section 4 |
Have another request? Contact LiberLite support from your account, or call/WhatsApp +255753099003.
Advanced: REST API v1 (grouped orders, idempotency keys)
A JSON REST API that can place several services for the same link in one order. Full reference: OpenAPI JSON
curl -X POST https://liberlite.com/api/v1/orders \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: campaign-001" \
-d '{"items":[{"service_id":1,"link":"https://instagram.com/p/XYZ","quantity":1000},{"service_id":3,"link":"https://instagram.com/p/XYZ","quantity":1000}]}'