コンテンツへスキップ

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
validbooleanWhether the license is valid and active
license_typestring|nullstandard
licensed_tostring|nullName of the license holder
domainstring|nullRegistered domain (if activated)
expires_atISO8601|nullLicense expiry date (null = perpetual)
support_expires_atISO8601|nullSupport/updates expiry date
support_activebooleanWhether support is still active
failure_reasonstring|nullReason 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.

  1. Step 1 — License Input: The installer shows a form asking for the license key. That's the only thing the buyer types.
  2. Step 2 — Server-Side Verification: Your installer calls the verification API from the backend (never from client-side JavaScript in production).
  3. Step 3 — Domain Activation: Pass the installation host in the domain parameter so the license is bound to this install.
  4. Step 4 — Mark Installed: On success, write a storage/installed marker 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 domain parameter for domain locking
  • Surface a clear, recoverable error on failure (network or invalid key)
  • Capture activations_used / activations_limit and support_active from 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.

Sign in to get a sandbox license

How to get one (for an existing product)

  1. Go to Author Dashboard → My Products → Edit on the product you want to test.
  2. Scroll to the Developer Test License card at the bottom.
  3. Click Generate. A UUID key is issued instantly, bound to your user + product.
  4. Copy the key into your installer and run the 6 verification scenarios below.
  5. 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 domain parameter 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/verify includes "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 domainFresh key, domain=localhostInstall proceeds, cache written
Valid key + already-activated domainSame key, same domain, re-runInstall proceeds, no double-activation
Slots exhaustedActivate 5 distinct local domains, try a 6thClear "deactivate existing" error
Invalid / unknown keyUse a random UUIDClear error, installer stays on License step
Expired keyLet the 30-day dev key lapse, or use an expired purchased keyClear "License has expired" error
Network failurePoint apptrovo.com to 127.0.0.1 in /etc/hostsFriendly error + retry — not a white screen
Before you ship: switch the base URL in your installer config from 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 Documentation

Installer SDK Package

Ready-to-use PHP installer with license verification, step-by-step wizard, and customizable branding.

Download SDK

What's in the SDK

File Copy To Purpose
AppTrovoLicense.phpapp/Services/License verification SDK — one method, one HTTP call
InstallController.phpapp/Http/Controllers/5-step installer (requirements, license, database, setup, done)
CheckLicenseMiddleware.phpapp/Http/Middleware/Installed-marker gate — never calls the API
views/*.blade.phpresources/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

1 Requirements
2 License
3 Database
4 Setup
5 Complete
  1. Step 1 — Requirements: Checks PHP version, extensions (BCMath, cURL, GD, PDO, etc.) and folder permissions (storage/, bootstrap/cache).
  2. 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.
  3. Step 3 — Database: Buyer enters MySQL credentials. The installer tests the connection before proceeding.
  4. Step 4 — Setup: Set app name, URL, and create the admin account. Runs migrations, seeds data, generates app key.
  5. 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.