Developer Integration Guide

How to connect WordPress plugins, PHP apps, Laravel projects, Android apps, Windows/macOS/Linux desktop apps, Electron apps, browser extensions, NPM/Python packages, Docker bundles, and generic projects to this update server.

Your API endpoint:

https://wordpress.refat.ovh/api/update.php

How It Works

This is a self-hosted update server. Instead of distributing plugins through WordPress.org or another marketplace, your apps call this server's API to check for updates and download new versions. The flow is:

  1. Your plugin/app calls the API with its current version and slug.
  2. The server compares that version against the latest stored version.
  3. If a newer version exists, the server returns metadata and a download URL.
  4. Your plugin/app downloads the release package. WordPress/PHP/Laravel usually receive generated ZIP files; Android packages receive the APK directly when an APK is uploaded; direct package projects such as Windows, macOS, Linux, Electron, browser extension, NPM, Python, Docker, and Generic receive the uploaded artifact directly and can also use a plain JSON release manifest feed.

Plugin slug rules: lowercase letters, numbers, and hyphens only (e.g. my-cool-plugin). For WordPress plugins, this must exactly match the plugin folder name and main file name.

Public vs Licensed Projects

Each project has its own access mode. A project can be Public / Free or Licensed. This is configured per project in the admin panel when creating or editing the project.

ModeUpdate checksDownloadsLicense Manager needed?
Public / FreeNo license key requiredAnyone can download from the project page or APINo. Public projects keep working even when License Manager is disabled or unavailable.
Licensedlicense_key requiredDownload is allowed only after an active key is verifiedYes. Uses the existing license-manager-laravel reseller API.

Important: License checking is not mandatory for every project. You can host many free/public projects and only protect selected paid projects. Enabling the License Manager connection only makes licensed projects verifiable; it does not force public projects to ask for a license.

For a licensed project, set Access Mode = Licensed and enter the License Manager App ID. The server calls the reseller API with action=get_license, then confirms the license is active and its app_id matches the project App ID.

// Public / Free request — no license key
POST https://wordpress.refat.ovh/api/update.php
action=update_check
request={"slug":"free-project","version":"1.0.0"}

// Licensed request — license key is required
POST https://wordpress.refat.ovh/api/update.php
action=update_check
request={"slug":"paid-project","version":"1.0.0","license_key":"YOUR-LICENSE-KEY"}

For licensed projects, failed verification returns HTTP 403:

HTTP/1.1 403 Forbidden
{ "success": false, "message": "License key is required for this licensed project." }

WordPress Plugin Integration

1. Create the updater class

Save this as class-plugin-updater.php inside your plugin folder. For public/free projects, pass an empty license key. For licensed projects, pass the license key stored in your plugin's option to the constructor.

<?php
if ( ! defined( 'ABSPATH' ) ) exit;

class My_Plugin_Updater {
    private $api_url = '', $plugin_file = '', $plugin_slug = '', $current_version = '', $license_key = '';

    public function __construct( $api_url, $plugin_file, $license_key = '' ) {
        $this->api_url         = trailingslashit( $api_url );
        $this->plugin_file     = $plugin_file;
        $this->plugin_slug     = basename( dirname( $plugin_file ) );
        $this->current_version = get_plugin_data( $plugin_file )['Version'];
        $this->license_key     = sanitize_text_field( $license_key );

        add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_for_update' ] );
        add_filter( 'plugins_api', [ $this, 'plugin_api_call' ], 10, 3 );
        add_action( 'upgrader_process_complete', [ $this, 'clear_update_transient' ], 10, 2 );
    }

    public function check_for_update( $transient ) {
        if ( empty( $transient->checked ) ) return $transient;
        $basename = plugin_basename( $this->plugin_file );
        $args     = [ 'slug' => $this->plugin_slug, 'version' => $this->current_version ];
        if ( $this->license_key !== '' ) $args['license_key'] = $this->license_key;
        $response = $this->api_request( 'update_check', $args );
        if ( $response ) {
            $key = version_compare( $response->new_version, $this->current_version, '>' ) ? 'response' : 'no_update';
            $transient->$key[ $basename ] = $response;
        }
        return $transient;
    }

    public function plugin_api_call( $result, $action, $args ) {
        if ( 'plugin_information' !== $action || ! isset( $args->slug ) || $args->slug !== $this->plugin_slug ) return $result;
        $request_args = [ 'slug' => $this->plugin_slug ];
        if ( $this->license_key !== '' ) $request_args['license_key'] = $this->license_key;
        return $this->api_request( 'plugin_information', $request_args ) ?: $result;
    }

    private function api_request( $action, $args ) {
        $response = wp_remote_post( $this->api_url, [
            'body'    => [ 'action' => $action, 'request' => wp_json_encode( $args ) ],
            'timeout' => 15,
        ]);
        if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) return false;
        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        return ( $body && ! empty( $body['success'] ) && ! empty( $body['data'] ) ) ? (object) $body['data'] : false;
    }

    public function clear_update_transient( $upgrader, $options ) {
        if ( isset( $options['action'], $options['type'], $options['plugins'] )
             && 'update' === $options['action'] && 'plugin' === $options['type']
             && in_array( plugin_basename( $this->plugin_file ), $options['plugins'], true )
        ) delete_site_transient( 'update_plugins' );
    }
}

2. Initialise in your main plugin file

<?php
/**
 * Plugin Name: Your Plugin Name
 * Version:     1.0.0
 * Requires PHP: 7.4
 */

require_once __DIR__ . '/class-plugin-updater.php';

function my_plugin_start_updater() {
    // Public/free project: keep this empty. Licensed project: read from your settings page.
    $license_key = get_option( 'my_plugin_license_key', '' );
    new My_Plugin_Updater( 'https://wordpress.refat.ovh/api/update.php', __FILE__, $license_key );
}
add_action( 'admin_init', 'my_plugin_start_updater' );

Testing tip: WordPress caches update checks for ~12 hours via its update_plugins transient. Force an immediate check via Dashboard → Updates → Check Again. The recommended updater class does not add its own cache on top of this — updates appear as soon as WordPress runs its next check.

3. Plugin metadata fields (info.json reference)

When you add a plugin via the admin panel, the server stores this JSON structure. Here's what each field does in the WordPress update dialog:

FieldUsed by WordPress forFormat
namePlugin name in update listPlain text
versionLatest version to compare againstSemver string e.g. 1.2.0
requiresMinimum WordPress version warninge.g. 5.8
requires_phpMinimum PHP version warninge.g. 7.4
tested"Tested up to" in the plugin popupe.g. 6.5
sections.descriptionDescription tab in plugin popupPlain text (HTML stripped)
sections.changelogChangelog tab in plugin popupHTML — h4, ul, li, strong, em, a allowed
banners.lowBanner image in plugin popup (772×250)Image URL or uploaded path
banners.highRetina banner (1544×500)Image URL or uploaded path
icons.1xPlugin icon in the list (128×128)Image URL or uploaded path
icons.2xRetina plugin icon (256×256)Image URL or uploaded path
license.modeProject access modepublic or licensed. Public is the default.
license.app_idLicense Manager App IDRequired only when license.mode is licensed.

PHP / Laravel Integration

Generic PHP

<?php
define('MY_SLUG',        'my-custom-app');
define('MY_VERSION',     '1.0.0');
// Public/free project: leave empty. Licensed project: set the customer's key.
define('MY_LICENSE_KEY', '');
$api_url = 'https://wordpress.refat.ovh/api/update.php';

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => $api_url,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query([
        'action'  => 'update_check',
        'request' => json_encode([
            'slug'    => MY_SLUG,
            'version' => MY_VERSION,
        ] + (MY_LICENSE_KEY !== '' ? ['license_key' => MY_LICENSE_KEY] : [])),
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 15,
    CURLOPT_SSL_VERIFYPEER => true,
]);
$result   = curl_exec($ch);
$http     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http === 403) {
    // Licensed project only: missing, invalid, inactive, expired, or wrong App ID
    $err = json_decode($result);
    die("License error: " . ($err->message ?? 'Access denied'));
}
$response = json_decode($result);
if ($response && $response->success && isset($response->data)) {
    $data = $response->data;
    if (version_compare($data->new_version, MY_VERSION, '>')) {
        echo "Update available: v" . $data->new_version . "\n";
        echo "Download: " . $data->package . "\n";
    }
}

Laravel Artisan Command

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;

class CheckProjectUpdate extends Command {
    protected $signature = 'project:check-update';

    public function handle(): int {
        $licenseKey = (string) config('services.updater.license_key', ''); // empty for public/free projects
        $response   = Http::timeout(15)->post('https://wordpress.refat.ovh/api/update.php', [
            'action'  => 'update_check',
            'request' => json_encode([
                'slug'    => 'my-laravel-app',
                'version' => '1.0.0',
            ] + ($licenseKey !== '' ? ['license_key' => $licenseKey] : [])),
        ]);
        if ($response->status() === 403) {
            $this->error("License error: " . $response->json('message'));
            return Command::FAILURE;
        }
        $data = $response->json('data');
        if ($data && version_compare($data['new_version'], '1.0.0', '>')) {
            $this->info("New version: " . $data['new_version']);
            $this->line("Download:    " . $data['package']);
            if (!empty($data['checksum'])) $this->line("Checksum:    " . $data['checksum']);
        } else {
            $this->info('Already up to date.');
        }
        return Command::SUCCESS;
    }
}

Android (Kotlin) Integration

Add <uses-permission android:name="android.permission.INTERNET" /> to your manifest, then:

Recommended Android release flow: upload your signed .apk in Admin → Upload New Version. The same package URL then downloads an APK directly, so users do not have to unzip a ZIP wrapper.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL

object UpdateChecker {
    private const val API_URL     = "https://wordpress.refat.ovh/api/update.php"
    private const val SLUG        = "my-android-app"
    private const val VERSION     = "1.0.0"
    private const val LICENSE_KEY = "" // public/free: empty; licensed: store securely, e.g. BuildConfig

    suspend fun checkForUpdate() = withContext(Dispatchers.IO) {
        try {
            val payload = "action=update_check&request=" +
                java.net.URLEncoder.encode(
                    JSONObject().put("slug", SLUG).put("version", VERSION).apply {
                        if (LICENSE_KEY.isNotBlank()) put("license_key", LICENSE_KEY)
                    }.toString(), "UTF-8"
                )
            val conn = (URL(API_URL).openConnection() as HttpURLConnection).apply {
                requestMethod = "POST"; connectTimeout = 15000; readTimeout = 15000
                doOutput = true; setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
            }
            OutputStreamWriter(conn.outputStream).use { it.write(payload) }
            if (conn.responseCode != 200) return@withContext null
            val json = JSONObject(conn.inputStream.bufferedReader().readText())
            if (!json.optBoolean("success")) return@withContext null
            val data = json.getJSONObject("data")
            Pair(data.optString("new_version"), data.optString("package"))
        } catch (e: Exception) { null }
    }
}

Windows App / UNI-SERVER Integration

Create a project with Type = Windows App, then upload each Windows release from the project edit page. The server stores the installer/app package directly and exposes both the normal update API and a plain Windows release-feed JSON endpoint.

Supported Windows uploads: .exe, .msi, .msix, .msixbundle, .appinstaller, and .zip.

For apps like UNI-SERVER, point the app's update-feed URL to:

https://wordpress.refat.ovh/api/update.php?action=windows_manifest&slug=uni-server

The manifest response is intentionally plain JSON so a desktop app can deserialize it directly:

{
  "name": "UNI SERVER",
  "channel": "stable",
  "version": "1.0.69",
  "tag": "win-v1.0.69",
  "company": "Uniselv",
  "developer": "Uniselv",
  "website": "https://uniselv.com",
  "repository": "",
  "generated_at": "2026-07-07T12:00:00+06:00",
  "features": ["Modern Windows app update delivery"],
  "download_url": "https://your-server.com/api/update.php?action=download_plugin&slug=uni-server&version=1.0.69",
  "checksum": "sha256:..."
}

If the Windows project is licensed, append &license_key=YOUR-LICENSE-KEY to the manifest URL or use the normal update_check POST API with a license key.

macOS, Linux, Electron, Extensions, NPM, Python, Docker & Generic Projects

Create the project using the matching type, upload the final release artifact from the project edit page, and use the generic release manifest endpoint when your app, installer, CLI, or deployment script needs a simple JSON feed.

Supported uploads by type:

  • macOS: .dmg, .pkg, .zip
  • Linux: .AppImage, .deb, .rpm, .tar.gz, .tgz, .zip
  • Electron: Windows/macOS/Linux installer formats including .exe, .msi, .dmg, .pkg, .AppImage, .deb, .rpm, .tar.gz, and .zip
  • Browser Extension: .crx, .xpi, .zip
  • NPM: .tgz, .tar.gz, .zip
  • Python: .whl, .tar.gz, .tgz, .zip, .exe, .msi
  • Docker: .tar, .tar.gz, .tgz, .zip, .yml, .yaml, .json
  • Generic: broad artifact support for anything that does not fit a dedicated type.

Generic release manifest URL:

https://wordpress.refat.ovh/api/update.php?action=release_manifest&slug=your-project-slug
{
  "name": "My Desktop App",
  "slug": "my-desktop-app",
  "type": "macos",
  "channel": "stable",
  "version": "2.1.0",
  "tag": "v2.1.0",
  "download_url": "https://your-server.com/api/update.php?action=download_plugin&slug=my-desktop-app&version=2.1.0",
  "checksum": "sha256:...",
  "file_name": "my-desktop-app-2.1.0.dmg",
  "features": ["One-line release highlight"]
}

Releasing a New Version

  1. Bump the version number in your plugin header or version constant.
  2. Build your release package. For WordPress/PHP/Laravel, upload a ZIP. For Android, upload the signed APK directly. For Windows/macOS/Linux/Electron/browser extension/NPM/Python/Docker/Generic projects, upload the final artifact directly so users or clients receive exactly the intended file.
  3. In the admin panel: go to your package → Add New Version → enter the version number → upload the package.
  4. The server automatically updates info.json to reflect the new latest version.
  5. Clients polling the API will receive the update on their next check.

Version comparison: The server uses PHP's version_compare(), which follows standard semver rules. 1.10.0 is correctly treated as greater than 1.9.0.

API Reference v1

All POST requests go to:

https://wordpress.refat.ovh/api/update.php

All responses are JSON and include "api_version": "v1". CORS is enabled (Access-Control-Allow-Origin: *).
Rate limits: 30 API calls / IP / minute  ·  10 downloads / IP / minute

POST update_check

Checks whether a newer version exists. Call this on application startup or periodically.

POST fieldValue
actionupdate_check
requestJSON string. Public example: {"slug":"your-slug","version":"1.0.0"}. Licensed example: {"slug":"your-slug","version":"1.0.0","license_key":"KEY"}. license_key is required only for projects whose access mode is licensed.

Response — always returns the latest version data:

{
  "success": true,
  "data": {
    "slug": "your-slug",
    "new_version": "1.2.0",
    "package": "https://your-server.com/api/update.php?action=download_plugin&slug=your-slug&version=1.2.0",
    "checksum": "sha256:a3f5c8...",
    "url": "https://your-server.com/plugin/your-slug",
    "tested": "6.7",
    "requires_php": "7.4"
  }
}

The server always returns the full data object with the latest version. Version comparison is the client's responsibility — compare data.new_version against your installed version using version_compare() to decide whether an update is available.

Response when the plugin slug is not found on the server:

{ "success": true, "data": null }

POST plugin_information

Returns full plugin metadata — used by WordPress for the plugin details popup.

POST fieldValue
actionplugin_information
requestJSON string. Public example: {"slug":"your-slug"}. Licensed example: {"slug":"your-slug","license_key":"KEY"}.

GET download_plugin

Streams the release file for a specific version. Public projects need only slug and version. Licensed projects must also include license_key=KEY. Android packages are served as direct APK downloads when an APK is available. Direct package projects are served as their uploaded artifact. WordPress, PHP, and Laravel releases are served as generated ZIP files.

// Public / Free download
GET https://wordpress.refat.ovh/api/update.php?action=download_plugin&slug=your-slug&version=1.2.0

// Licensed download
GET https://wordpress.refat.ovh/api/update.php?action=download_plugin&slug=your-slug&version=1.2.0&license_key=YOUR-LICENSE-KEY

GET windows_manifest

Returns plain Windows release-feed JSON for Windows desktop apps. Use this for UNI-SERVER style apps that expect a direct manifest URL instead of the normal wrapped API response.

// Public Windows manifest
GET https://wordpress.refat.ovh/api/update.php?action=windows_manifest&slug=uni-server

// Licensed Windows manifest
GET https://wordpress.refat.ovh/api/update.php?action=windows_manifest&slug=uni-server&license_key=YOUR-LICENSE-KEY

GET release_manifest

Returns plain release-feed JSON for manifest-enabled project types: Windows, macOS, Linux, Electron, browser extension, NPM, Python, Docker, and Generic.

// Public generic release manifest
GET https://wordpress.refat.ovh/api/update.php?action=release_manifest&slug=your-project-slug

// Licensed generic release manifest
GET https://wordpress.refat.ovh/api/update.php?action=release_manifest&slug=your-project-slug&license_key=YOUR-LICENSE-KEY

GET ping

Health check — see the Status Endpoint section below.

Error HTTP codes

CodeMeaning
400Bad request — missing or invalid parameters
403Forbidden — license key missing, invalid, or inactive; or path traversal detected
404Plugin or version not found
405Method not allowed
429Rate limit exceeded — back off and retry
500Server error

Checksum Verification

Every update_check response includes a checksum field (sha256:<hex>). Always verify this before installing a downloaded package to ensure the file wasn't tampered with in transit.

// PHP example
$file     = '/tmp/plugin-update.zip';
file_put_contents($file, file_get_contents($data->package));

$expected = str_replace('sha256:', '', $data->checksum);
$actual   = hash_file('sha256', $file);

if (!hash_equals($expected, $actual)) {
    throw new RuntimeException('Checksum mismatch — aborting install.');
}

$zip = new ZipArchive();
$zip->open($file);
$zip->extractTo('/path/to/install/');
$zip->close();

Rollback / Specific Version Downloads

Every uploaded version is individually downloadable. Public projects can be downloaded directly. Licensed projects require the same license_key query parameter for rollback downloads.

GET https://wordpress.refat.ovh/api/update.php?action=download_plugin&slug=your-slug&version=1.0.0

The plugin detail page on this server lists all available versions with individual download buttons.

Admin Management Features

The admin dashboard includes scalable controls for large plugin repositories:

  • Search and filters: search by name, slug, type, author, status, version, requirements, and tested version. Use the quick filter tabs for active, inactive, sync-needed, and failed-log packages.
  • Active/inactive packages: inactive packages stay in admin but are hidden from the public repository and return no update/download availability through the API.
  • Public/licensed access: each package can be public/free or licensed. Public packages bypass license checks; licensed packages require an active license key and matching App ID from the reseller API.
  • Bulk actions: refresh update status, sync metadata to the latest local version tag, activate/deactivate, clear package cache, create backups, or delete selected plugins.
  • Safety backups: backups are created before upload, delete, restore, and bulk delete actions. Backups are stored under data/backups/{slug}/.
  • Rollback: open Admin → Edit Plugin → Safety Backups and restore a previous ZIP backup. The current state is backed up before restore.
  • History: Admin → Logs shows release, backup, restore, delete, cache, metadata, and failed-upload history with single-delete and failed-only cleanup options.

The package status is stored in plugins/{slug}/info.json as "status": "active" or "status": "inactive". Older packages without this field are treated as active.

Health / Status Endpoint

GET https://wordpress.refat.ovh/api/update.php?action=ping

Returns HTTP 200 and a JSON body when the server is healthy:

{
  "success": true,
  "status": "ok",
  "api_version": "v1",
  "plugins": 4,
  "server_time": "2025-10-15T12:00:00+00:00"
}

Compatible with UptimeRobot, Better Uptime, and any HTTP health-check service. The full visual status dashboard is available at https://wordpress.refat.ovh/api/status.