Developer Integration Guide
How to connect WordPress plugins, PHP apps, Laravel projects, or Android apps to this update server.
Your API endpoint:
Table of Contents
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:
- Your plugin/app calls the API with its current version and slug.
- The server compares that version against the latest stored version.
- If a newer version exists, the server returns metadata and a download URL.
- Your plugin/app downloads the ZIP and installs it โ or (for WordPress) hands it to WordPress core to handle.
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.
WordPress Plugin Integration
1. Create the updater class
Save this as class-plugin-updater.php inside your plugin folder.
<?php
if ( ! defined( 'ABSPATH' ) ) exit;
class My_Plugin_Updater {
private $api_url = '', $plugin_file = '', $plugin_slug = '', $current_version = '';
public function __construct( $api_url, $plugin_file ) {
$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'];
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 );
$response = $this->api_request( 'update_check', [ 'slug' => $this->plugin_slug, 'version' => $this->current_version ] );
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;
return $this->api_request( 'plugin_information', [ 'slug' => $this->plugin_slug ] ) ?: $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() {
new My_Plugin_Updater( 'https://wordpress.refat.ovh/api/update.php', __FILE__ );
}
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:
| Field | Used by WordPress for | Format |
|---|---|---|
name | Plugin name in update list | Plain text |
version | Latest version to compare against | Semver string e.g. 1.2.0 |
requires | Minimum WordPress version warning | e.g. 5.8 |
requires_php | Minimum PHP version warning | e.g. 7.4 |
tested | "Tested up to" in the plugin popup | e.g. 6.5 |
sections.description | Description tab in plugin popup | Plain text (HTML stripped) |
sections.changelog | Changelog tab in plugin popup | HTML โ h4, ul, li, strong, em, a allowed |
banners.low | Banner image in plugin popup (772ร250) | Image URL or uploaded path |
banners.high | Retina banner (1544ร500) | Image URL or uploaded path |
icons.1x | Plugin icon in the list (128ร128) | Image URL or uploaded path |
icons.2x | Retina plugin icon (256ร256) | Image URL or uploaded path |
PHP / Laravel Integration
Generic PHP
<?php
define('MY_SLUG', 'my-custom-app');
define('MY_VERSION', '1.0.0');
$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]),
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_SSL_VERIFYPEER => true,
]);
$result = curl_exec($ch);
curl_close($ch);
$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 {
$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']),
]);
$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:
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"
suspend fun checkForUpdate() = withContext(Dispatchers.IO) {
try {
val payload = "action=update_check&request=" +
java.net.URLEncoder.encode(
JSONObject().put("slug", SLUG).put("version", VERSION).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 }
}
}
Releasing a New Version
- Bump the version number in your plugin header or version constant.
- Build your release ZIP โ it should contain the plugin folder at its root (e.g.
my-plugin/my-plugin.php). - In the admin panel: go to your plugin โ Add New Version โ enter the version number โ upload the ZIP.
- The server automatically updates
info.jsonto reflect the new latest version. - 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:
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 field | Value |
|---|---|
action | update_check |
request | JSON string: {"slug":"your-slug","version":"1.0.0"} |
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 field | Value |
|---|---|
action | plugin_information |
request | JSON string: {"slug":"your-slug"} |
GET download_plugin
Streams the ZIP file for a specific version directly to the client.
GET https://wordpress.refat.ovh/api/update.php?action=download_plugin&slug=your-slug&version=1.2.0
GET ping
Health check โ see the Status Endpoint section below.
Error HTTP codes
| Code | Meaning |
|---|---|
400 | Bad request โ missing or invalid parameters |
403 | Forbidden โ path traversal detected |
404 | Plugin or version not found |
405 | Method not allowed |
429 | Rate limit exceeded โ back off and retry |
500 | Server 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 version with a source/ folder on the server is individually downloadable. Pass the exact version string to the download endpoint:
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.
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",
"php_version": "8.2.0",
"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.