License Verification & Integration Guide
Protect your products sold on AppTrovo by integrating our license verification API into your application installer or admin panel.
Overview
Every product purchased on AppTrovo is issued a unique license key (UUID format). Authors can verify these keys using our public REST API to ensure buyers have a valid, active license before granting access to features, updates, or support.
Base URL
https://apptrovo.com/api/v1/licenses/verify
Quick Start
A single GET request is all you need. product_slug is optional — omit it to let the API resolve the product from the key and return it in the response.
GET https://apptrovo.com/api/v1/licenses/verify?license_key=YOUR_KEY
Response (valid):
{
"valid": true,
"license_id": 142,
"product_id": 28,
"product": {
"id": 28,
"slug": "your-product-slug",
"name": "Your Product",
"retired": false
},
"license_type": "standard",
"licensed_to": "Acme Corp",
"domain": "acme.com",
"expires_at": null,
"support_expires_at": "2027-04-08T00:00:00Z",
"support_active": true,
"activations_used": 1,
"activations_limit": 3,
"failure_reason": null
}
Response (invalid):
{
"valid": false,
"failure_reason": "Invalid license key."
}
For authors using the Laravel SDK
Buyers never type a product slug. You set product_slug once in
config/apptrovo.php before zipping your product — the SDK reads
it from there. Your buyers only enter their license key on /install
and are done.
Request Parameters
| Parameter | Required | Type | Description |
|---|---|---|---|
license_key |
Yes | string (max 64) | The UUID license key provided to the buyer after purchase |
product_slug |
No | string (max 255) | Optional. When sent, enforces the license belongs to this product (recommended for author-side defense). Omit to let the server resolve the product from the key. |
domain |
No | string (max 255) | Domain to validate against the license registration (optional) |
Response Fields
| Field | Type | Description |
|---|---|---|
valid | boolean | Whether the license is valid and active |
license_type | string|null | standard |
licensed_to | string|null | Name of the license holder |
domain | string|null | Registered domain (if activated) |
expires_at | ISO8601|null | License expiry date (null = perpetual) |
support_expires_at | ISO8601|null | Support/updates expiry date |
support_active | boolean | Whether support is still active |
failure_reason | string|null | Reason for failure (only when valid=false) |
Failure Reasons
| Reason | Cause |
|---|---|
Product not found. | The product_slug doesn't match any product |
Invalid license key for this product. | Key doesn't exist or doesn't belong to this product |
License has been deactivated. | Admin or system deactivated the license |
License has expired. | The expires_at date has passed |
License is registered to a different domain. | Domain mismatch (only when both request and license have domains) |
Product Lifecycle — product.retired
Every verify response carries a product.retired flag. This exists because a
buyer's license is a permanent sale record — it must keep verifying indefinitely, even if
the product later disappears from the marketplace.
false(normal) — the product is still for sale on AppTrovo. Business as usual.true— the product has been retired (unlisted) by the author or admin, but the buyer's license is still valid. The API resolves the product identity from a permanent snapshot captured when the license was issued, so verification keeps succeeding.
Capture this flag from the verify response during the installer and persist it into your product's settings table — that way your admin UI can render a soft banner without ever calling the API again:
// In your InstallController, after $result = $license->verify(...)
Setting::set('apptrovo.product_retired', $result['product']['retired'] ?? false);
// Later, in your product's admin layout
if (Setting::get('apptrovo.product_retired')) {
// Render a dismissible notice:
// "This product has been retired on AppTrovo. Your license remains
// valid — no further updates are expected."
}
For authors — Retire vs Delete
Once a product has issued at least one license, it can no longer be hard-deleted — the sale record must survive. The strongest action available is Retire, which unlists the product from the marketplace but leaves every existing license fully functional. This matches how CodeCanyon / Envato handle unlisted items.
Rate Limiting
The API allows 120 requests per minute per IP address. Cache verification results locally to avoid hitting limits.
Integration Examples
PHP (Laravel / Plain PHP)
/**
* Verify a license key against AppTrovo API.
*/
function verifyLicense(string $licenseKey, string $productSlug, ?string $domain = null): array
{
$params = [
'license_key' => $licenseKey,
'product_slug' => $productSlug,
];
if ($domain) {
$params['domain'] = $domain;
}
$url = 'https://apptrovo.com/api/v1/licenses/verify?' . http_build_query($params);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$response) {
return ['valid' => false, 'failure_reason' => 'Unable to reach license server.'];
}
return json_decode($response, true);
}
// Usage in your installer or admin panel:
$result = verifyLicense(
$_POST['license_key'],
'my-awesome-saas-script',
$_SERVER['HTTP_HOST'] ?? null
);
if ($result['valid']) {
// License is valid — proceed with installation
// Optionally store license info locally for offline checks
file_put_contents('storage/license.json', json_encode([
'license_key' => $_POST['license_key'],
'license_type' => $result['license_type'],
'licensed_to' => $result['licensed_to'],
'verified_at' => date('c'),
]));
} else {
// Show error to user
die('License verification failed: ' . $result['failure_reason']);
}
JavaScript / Node.js
async function verifyLicense(licenseKey, productSlug, domain = null) {
const params = new URLSearchParams({
license_key: licenseKey,
product_slug: productSlug,
});
if (domain) params.set('domain', domain);
try {
const res = await fetch(
`https://apptrovo.com/api/v1/licenses/verify?${params}`
);
return await res.json();
} catch (err) {
return { valid: false, failure_reason: 'Unable to reach license server.' };
}
}
// Usage
const result = await verifyLicense('your-uuid-key', 'my-product-slug', 'client-domain.com');
if (result.valid) {
console.log(`Licensed to: ${result.licensed_to}, Type: ${result.license_type}`);
} else {
console.error(`Invalid: ${result.failure_reason}`);
}
Python
import requests
def verify_license(license_key: str, product_slug: str, domain: str = None) -> dict:
params = {"license_key": license_key, "product_slug": product_slug}
if domain:
params["domain"] = domain
try:
r = requests.get("https://apptrovo.com/api/v1/licenses/verify", params=params, timeout=10)
return r.json()
except Exception:
return {"valid": False, "failure_reason": "Unable to reach license server."}
# Usage
result = verify_license("your-uuid-key", "my-product-slug", "client-domain.com")
if result["valid"]:
print(f"Licensed to: {result['licensed_to']}")
else:
print(f"Invalid: {result['failure_reason']}")
Installer Integration Pattern
The recommended pattern is verify once, at install time. After the web installer completes, your product runs without ever calling the AppTrovo API again — no periodic recheck, no offline grace period to think about, no licensing failure mode that can break a buyer's production app because of a network blip.
- Step 1 — License Input: The installer shows a form asking for the license key. That's the only thing the buyer types.
- Step 2 — Server-Side Verification: Your installer calls the verification API from the backend (never from client-side JavaScript in production).
- Step 3 — Domain Activation: Pass the installation host in the
domainparameter so the license is bound to this install. - Step 4 — Mark Installed: On success, write a
storage/installedmarker file and complete the rest of the installer (database, admin user, etc.). After this point, no further licensing logic runs.
Example: Laravel Installer + Runtime Middleware
The Laravel Installer SDK ships a
ready-made installer controller plus a tiny middleware that only checks for the
storage/installed marker. You set your product slug once in
config/apptrovo.php — buyers don't touch .env at all.
// config/apptrovo.php (author, committed to repo)
<?php
return [
'product_slug' => 'your-product-slug',
];
// app/Http/Controllers/InstallController.php (excerpt — runs once during /install)
public function verifyLicense(Request $request)
{
$request->validate(['license_key' => 'required|string|max:64']);
$license = new \App\Services\AppTrovoLicense(); // slug auto-loaded from config
$result = $license->verify(
$request->input('license_key'),
$request->getHost()
);
if (! $result['valid']) {
return back()->withInput()->withErrors([
'license_key' => $result['failure_reason'],
]);
}
// Stash for the rest of the installer steps; storage/installed is
// written at the end of step 4 (Setup) — that's the marker the
// runtime middleware looks at.
session(['install_license_key' => $request->input('license_key')]);
return redirect()->route('install.database');
}
// app/Http/Middleware/CheckLicenseMiddleware.php — runtime gate (no API calls)
class CheckLicenseMiddleware
{
public function handle($request, Closure $next)
{
if ($request->is('install*')) {
return $next($request);
}
if (! file_exists(storage_path('installed'))) {
return redirect('/install');
}
return $next($request);
}
}
Best Practices
Do
- Verify server-side (never client-only)
- Verify exactly once — during the web installer
- Use the
domainparameter for domain locking - Surface a clear, recoverable error on failure (network or invalid key)
- Capture
activations_used/activations_limitandsupport_activefrom the verify response if your product uses them
Don't
- Don't re-verify on every page load (verify-once is the contract)
- Don't build a background re-check / cron — it only creates failure modes for the buyer
- Don't hardcode your product slug client-side
- Don't store the license key in public JavaScript
- Don't skip SSL verification in production
- Don't ignore the rate limit (120/min)
Feature Gating & Activation Info
The verify response includes license_type, activations_used,
activations_limit, support_expires_at, and support_active.
Since the SDK only verifies at install time, capture whichever fields you care about
into your product's own settings table during the installer flow, then read them at
runtime as plain DB columns:
// During the installer (one-time, after verify succeeds)
Setting::set('license.type', $result['license_type']);
Setting::set('license.support_expires', $result['support_expires_at']);
Setting::set('license.activations_used', $result['activations_used'] ?? null);
// Later, anywhere in your product
if (Setting::get('license.type') === 'developer') {
// e.g. show a "dev install" badge in the admin header
}
Checking for Product Updates
The public product API does not require a license key, so you can poll it from your product's admin UI to check whether a newer version is available:
// Check for updates via the public product API
$product = json_decode(
file_get_contents('https://apptrovo.com/api/v2/products/your-product-slug'),
true
);
$latestVersion = $product['data']['current_version'] ?? null;
$installedVersion = config('app.version'); // Your local version
if ($latestVersion && version_compare($latestVersion, $installedVersion, '>')) {
// New version available — prompt for update
}
Testing Your Integration
Use the test environment to verify your integration before going live:
Test API: https://mp.chetsapp.de/api/v1/licenses/verify
Production API: https://apptrovo.com/api/v1/licenses/verify
Developer Test License
Before submitting a product for review, you need to test that your installer handles the full license-verification flow end-to-end — but you don't have a paid license for your own product yet. Every author can issue themselves a developer test license for any of their own products, for free.
No product yet? Use the SDK sandbox.
Brand-new authors can exercise the verify flow against a shared sandbox product before creating their first listing.
You'll test end-to-end now, then do a one-line swap of the product_slug when your real product goes live.
How to get one (for an existing product)
- Go to Author Dashboard → My Products → Edit on the product you want to test.
- Scroll to the Developer Test License card at the bottom.
- Click Generate. A UUID key is issued instantly, bound to your user + product.
- Copy the key into your installer and run the 6 verification scenarios below.
- Re-click Extend 30 days at any time to roll the expiry forward — the key stays the same.
Limitations (by design)
- Only verifies when the
domainparameter is a local hostname:localhost,127.0.0.1,::1, or ends in.test,.local,.localhost. Hitting the API with a real public domain returns a clear rejection — so the key can't be used in production. - 30-day expiry, extendable on demand.
- Up to 5 activation slots (enough for a few concurrent local test environments).
- One dev license per
(author, product)pair — re-clicking rotates the expiry, it doesn't issue a new key. - Response from
/api/v1/licenses/verifyincludes"license_type": "developer"so your SDK can log/branch on it if you want.
Quick smoke test
# Valid local domain — should return "valid": true
curl "https://apptrovo.com/api/v1/licenses/verify?license_key=YOUR_DEV_KEY&product_slug=your-product&domain=localhost"
# Real domain — must reject because this is a dev license
curl "https://apptrovo.com/api/v1/licenses/verify?license_key=YOUR_DEV_KEY&product_slug=your-product&domain=example.com"
# → "valid": false, "failure_reason": "This is a developer license and can only activate on local domains..."
The 6 scenarios to run before submitting
| Scenario | How | Expected UX |
|---|---|---|
| Valid key + new local domain | Fresh key, domain=localhost | Install proceeds, cache written |
| Valid key + already-activated domain | Same key, same domain, re-run | Install proceeds, no double-activation |
| Slots exhausted | Activate 5 distinct local domains, try a 6th | Clear "deactivate existing" error |
| Invalid / unknown key | Use a random UUID | Clear error, installer stays on License step |
| Expired key | Let the 30-day dev key lapse, or use an expired purchased key | Clear "License has expired" error |
| Network failure | Point apptrovo.com to 127.0.0.1 in /etc/hosts | Friendly error + retry — not a white screen |
https://mp.chetsapp.de (test) to https://apptrovo.com (production),
and remove or guard any APPTROVO_DEV_BYPASS-style flags you may have added during development.
Laravel Installer SDK (Drop-in Package)
We provide a complete, ready-to-use installer package for Laravel products. It includes a 5-step web installer, license verification, and runtime middleware.
Download the Installer SDK
Contains: License SDK class, InstallController, CheckLicenseMiddleware, 5 Blade views, README.
View SDK DocumentationInstaller SDK Package
Ready-to-use PHP installer with license verification, step-by-step wizard, and customizable branding.
What's in the SDK
| File | Copy To | Purpose |
|---|---|---|
AppTrovoLicense.php | app/Services/ | License verification SDK — one method, one HTTP call |
InstallController.php | app/Http/Controllers/ | 5-step installer (requirements, license, database, setup, done) |
CheckLicenseMiddleware.php | app/Http/Middleware/ | Installed-marker gate — never calls the API |
views/*.blade.php | resources/views/installer/ | Installer UI with Tailwind CSS |
Installation Steps (5 minutes)
Step 1: Copy the SDK files into your Laravel project:
cp AppTrovoLicense.php your-app/app/Services/AppTrovoLicense.php
cp InstallController.php your-app/app/Http/Controllers/InstallController.php
cp CheckLicenseMiddleware.php your-app/app/Http/Middleware/CheckLicenseMiddleware.php
cp -r views/ your-app/resources/views/installer/
Step 2: Create config/apptrovo.php with your product slug (author-baked, committed to your repo — buyers never edit this):
<?php
return [
// Your product's URL slug on AppTrovo
'product_slug' => 'your-product-slug',
];
That's the entire config. No recheck_days, no offline_grace_days,
and no APPTROVO_PRODUCT_SLUG / APPTROVO_API_URL /
APPTROVO_TIMEOUT env vars — the API URL and HTTP timeout are hardcoded
inside the SDK as direct values because they are not buyer-tunable. The buyer never
edits .env for licensing at all.
Step 3: Add installer routes to routes/web.php:
use App\Http\Controllers\InstallController;
Route::middleware('web')->prefix('install')->group(function () {
Route::get('/', [InstallController::class, 'requirements'])->name('install.requirements');
Route::get('/license', [InstallController::class, 'license'])->name('install.license');
Route::post('/license', [InstallController::class, 'verifyLicense'])->name('install.verifyLicense');
Route::get('/database', [InstallController::class, 'database'])->name('install.database');
Route::post('/database',[InstallController::class, 'saveDatabase'])->name('install.saveDatabase');
Route::get('/setup', [InstallController::class, 'setup'])->name('install.setup');
Route::post('/setup', [InstallController::class, 'install'])->name('install.install');
Route::get('/complete', [InstallController::class, 'complete'])->name('install.complete');
});
Step 4: Register the middleware in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'license' => \App\Http\Middleware\CheckLicenseMiddleware::class,
]);
})
Step 5: Protect your app routes:
// All your application routes behind license check
Route::middleware('license')->group(function () {
Route::get('/', [HomeController::class, 'index']);
// ... rest of your routes
});
Installer Flow
- Step 1 — Requirements: Checks PHP version, extensions (BCMath, cURL, GD, PDO, etc.) and folder permissions (storage/, bootstrap/cache).
- Step 2 — License: Buyer enters their AppTrovo license key. The SDK calls our API to verify it and binds the license to the installation domain.
- Step 3 — Database: Buyer enters MySQL credentials. The installer tests the connection before proceeding.
- Step 4 — Setup: Set app name, URL, and create the admin account. Runs migrations, seeds data, generates app key.
- Step 5 — Complete: Success page with post-install checklist (remove installer routes, set up cron, etc.).
Runtime Middleware
The runtime CheckLicenseMiddleware intentionally does
nothing license-related. It only verifies that the installer has
been run by checking for the storage/installed marker file:
- If the marker is missing → redirect to
/install - If the marker is present → allow the request through
No API calls, no cache file, no timing windows. License verification happens exactly once — in step 2 of the installer. After install, the AppTrovo API can be unreachable for months and the buyer's product will keep running normally.
Capturing Verify Fields For Later
If you want to use license metadata (type, support expiry, retired flag) after the installer is done, persist the relevant fields from the verify response into your product's own settings table during step 2 — then read them as plain DB values at runtime:
// In InstallController::verifyLicense(), right after a successful $result
Setting::set('license.type', $result['license_type']);
Setting::set('license.licensed_to', $result['licensed_to']);
Setting::set('license.support_expires', $result['support_expires_at']);
Setting::set('license.product_retired', $result['product']['retired'] ?? false);
// Anywhere in your product, no API call needed
if (Setting::get('license.type') === 'developer') {
// Show a "dev install" badge in the admin header
}
Need Help?
If you need assistance integrating license verification into your product, visit our Help Center or contact support@apptrovo.com.