Self-Hosted Plugin Updates
Before all – if any of the explanations in this tutorial seems difficult for you, you can always get any of my plugins and explore the code.
Step 1. Create a .json File with Update Information and Plugin ZIP Archive on Your Server
WordPress Update API accepts certain parameters like plugin name, plugin author, changelog etc. You can:
- Specify some of them, the static ones, directly in your plugin files (Plugin Author, Plugin Name..) and get the other ones from an update server (Download URL, New Version, Last Updated).
- Or you can get everything from your update server.
In this tutorial I will show you the first way, because it is more clear and simple to understand.
First of all your have to create a .json file somewhere on your server, for example YOUR_WEBSITE/info.json
. Please take a look at the parameters which I think really should be on your server:
{ "version" => "1.1", "download_url" : "https://rudrastyh.com/misha-plugin.zip", "requires" : "3.0", "tested" : "4.8.1", "requires_php" : "5.3", "last_updated" : "2017-08-17 02:10:00", "sections" : { "description" : "This is the plugin to test your updater script", "installation" : "Upload the plugin to your blog, Activate it, that's it!", "changelog" : "<h4>1.1 – January 17, 2020</h4><ul><li>Some bugs are fixed.</li><li>Release date.</li></ul>" }, "banners" : { "low" : "https://YOUR_WEBSITE/banner-772x250.jpg", "high" : "https://YOUR_WEBSITE/banner-1544x500.jpg" }, "screenshots" : "<ol><li><a href='IMG_URL' target='_blank'><img src='IMG_URL' alt='CAPTION' /></a><p>CAPTION</p></li></ol>" }
There are a plenty of other parameters as well, all of them I describe below. And it is on your choice which of them to specify in your info.json
file.
Also an important note about the files and folders structure inside a zip archive.
No 👿
- misha-plugin.zip
- misha-plugin.php
- readme.txt
Yes 😊
- misha-plugin.zip
- misha-plugin/
- misha-plugin.php
- readme.txt
- misha-plugin/
Here is what happened if you skip this advise: after you click the “Update” button, your old version of the plugin will be removed, but the new one won’t replace it but will be created in a randomly generated folder like misha-plugin-xhfuif/
as a result you will get a notice: “The misha-plugin.php has been deactivated due to an error: Plugin file does not exist.”

Step 2. Plugin Information for the Popup
When you click “View Details”, WordPress shows you the appropriate plugin information from its own server. And the most interesting thing is that this info can be hooked. It can be hooked before WordPress connects to its own server. So, it doesn’t affect website performance. While exploring WP code, I begin to love it more and more.

add_filter('plugins_api', 'misha_plugin_info', 20, 3); /* * $res empty at this step * $action 'plugin_information' * $args stdClass Object ( [slug] => woocommerce [is_ssl] => [fields] => Array ( [banners] => 1 [reviews] => 1 [downloaded] => [active_installs] => 1 ) [per_page] => 24 [locale] => en_US ) */ function misha_plugin_info( $res, $action, $args ){ // do nothing if this is not about getting plugin information if( 'plugin_information' !== $action ) { return false; } $plugin_slug = 'YOUR PLUGIN SLUG'; // we are going to use it in many places in this function // do nothing if it is not our plugin if( $plugin_slug !== $args->slug ) { return false; } // trying to get from cache first if( false == $remote = get_transient( 'misha_update_' . $plugin_slug ) ) { // info.json is the file with the actual plugin information on your server $remote = wp_remote_get( 'https://rudrastyh.com/wp-content/uploads/info.json', array( 'timeout' => 10, 'headers' => array( 'Accept' => 'application/json' ) ) ); if ( ! is_wp_error( $remote ) && isset( $remote['response']['code'] ) && $remote['response']['code'] == 200 && ! empty( $remote['body'] ) ) { set_transient( 'misha_update_' . $plugin_slug, $remote, 43200 ); // 12 hours cache } } if( ! is_wp_error( $remote ) && isset( $remote['response']['code'] ) && $remote['response']['code'] == 200 && ! empty( $remote['body'] ) ) { $remote = json_decode( $remote['body'] ); $res = new stdClass(); $res->name = $remote->name; $res->slug = $plugin_slug; $res->version = $remote->version; $res->tested = $remote->tested; $res->requires = $remote->requires; $res->author = '<a href="https://rudrastyh.com">Misha Rudrastyh</a>'; $res->author_profile = 'https://profiles.wordpress.org/rudrastyh'; $res->download_link = $remote->download_url; $res->trunk = $remote->download_url; $res->requires_php = '5.3'; $res->last_updated = $remote->last_updated; $res->sections = array( 'description' => $remote->sections->description, 'installation' => $remote->sections->installation, 'changelog' => $remote->sections->changelog // you can add your custom sections (tabs) here ); // in case you want the screenshots tab, use the following HTML format for its content: // <ol><li><a href="IMG_URL" target="_blank"><img src="IMG_URL" alt="CAPTION" /></a><p>CAPTION</p></li></ol> if( !empty( $remote->sections->screenshots ) ) { $res->sections['screenshots'] = $remote->sections->screenshots; } $res->banners = array( 'low' => 'https://YOUR_WEBSITE/banner-772x250.jpg', 'high' => 'https://YOUR_WEBSITE/banner-1544x500.jpg' ); return $res; } return false; }
Let me describe some more parameters that are not mentioned in the code:
- contributors
- The associative array of contributors wordpress.org profile name and URLs, like
array( 'rudrastyh' => 'https://profiles.wordpress.org/rudrastyh', 'contributer' => 'https://profiles.wordpress.org/contributer' )
- requires_php
- The minimum required version of PHP
- rating
- The rating count from 1 to 100
- ratings
- The 5-star rating, how much votes for each star, example:
array( 5 => 2104 4 => 116, 3 => 64, 2 => 57, 1 => 175 )
- num_ratings
- The amount of overall votes
- support_threads
- The number of threads on support forum
- support_threads_resolved
- The number of resolved threads on support forum
- active_installs
- The number of active plugin installations
- added
- The date in YYYY-MM-DD format
- homepage
- The plugin homepage URL
- reviews
- The HTML of plugin reviews. Review example:
- versions
- The array of plugin versions and their URLs, example
array( '1.0' => 'URL of zip archive of 1.0 plugin version', 'trunk' => 'URL of the latest plugin version' )
- donate_link
- The URL of donate link
Step 3. Push the Update Information into WP Transients

add_filter('site_transient_update_plugins', 'misha_push_update' ); function misha_push_update( $transient ){ if ( empty($transient->checked ) ) { return $transient; } // trying to get from cache first, to disable cache comment 10,20,21,22,24 if( false == $remote = get_transient( 'misha_upgrade_YOUR_PLUGIN_SLUG' ) ) { // info.json is the file with the actual plugin information on your server $remote = wp_remote_get( 'https://YOUR_WEBSITE/SOME_PATH/info.json', array( 'timeout' => 10, 'headers' => array( 'Accept' => 'application/json' ) ) ); if ( !is_wp_error( $remote ) && isset( $remote['response']['code'] ) && $remote['response']['code'] == 200 && !empty( $remote['body'] ) ) { set_transient( 'misha_upgrade_YOUR_PLUGIN_SLUG', $remote, 43200 ); // 12 hours cache } } if( $remote ) { $remote = json_decode( $remote['body'] ); // your installed plugin version should be on the line below! You can obtain it dynamically of course if( $remote && version_compare( '1.0', $remote->version, '<' ) && version_compare($remote->requires, get_bloginfo('version'), '<' ) ) { $res = new stdClass(); $res->slug = 'YOUR_PLUGIN_SLUG'; $res->plugin = 'YOUR_PLUGIN_FOLDER/YOUR_PLUGIN_SLUG.php'; // it could be just YOUR_PLUGIN_SLUG.php if your plugin doesn't have its own directory $res->new_version = $remote->version; $res->tested = $remote->tested; $res->package = $remote->download_url; $transient->response[$res->plugin] = $res; //$transient->checked[$res->plugin] = $remote->version; } } return $transient; }
Step 4. Cache the results to make it awesomely fast
The last step is to clean the cache after the plugin has been updated.
add_action( 'upgrader_process_complete', 'misha_after_update', 10, 2 ); function misha_after_update( $upgrader_object, $options ) { if ( $options['action'] == 'update' && $options['type'] === 'plugin' ) { // just clean the cache when new plugin version is installed delete_transient( 'misha_upgrade_YOUR_PLUGIN_SLUG' ); } }
Comments — 35
Hi, Misha! Excellent article, I’ve been looking for this solution for a long time. There is one question – on what principle is the file json
Are there any requirements for it?
Hi,
Thank you!
There are no rules for json, because you get it in your PHP code and restructure the way you need. It just must be valid :)
I plan to publish a tutorial with tips how to generate json in PHP and return different data depending on the parameters you sent in it, so, it is the way to check license keys, update expiration date etc.
OK got it. Thank you, Misha
Searching everywhere, but
results in $actions is not defined. This is what the WP code reference says:
$action is never defined, I also checked your test plugin.
Am I missing something?
Hmmm… Never faced with this error notice.
I solved the issue above. Now I’m facing a different problem with the
hook. It’s firing on all plugins except mine (When I click View Details on another plugin, I see my own plugin info). No idea why it’s happening. I can also see the update notification bubble in the menu because of the
but the update message doesn’t show on my plugin.
More detail here: https://github.com/DevinVinson/WordPress-Plugin-Boilerplate/issues/477
Perhaps you’ve ran into this before? I know you’re busy, but I’d love to hear your thoughts 🤓
It is because the hook is fired for every plugin. You have to add some conditionals inside the callback function 👍
Example: here, lines 14-15.
Yeah I figured. And I am using conditions (see code on that Github issue), I debugged $args->slug and my own plugin slug and they both match. Still it’s not passing that condition 🤔
Great! Thanks for an article!
This is awesome. Thanks.
the test plugin does not work with WP5..
there is a fix?
Seems Ok with WP 5.0.1
I try it in more sites and servers but does not work.. not show “more details” and update message. only the badge “1” on “Plugin” admin menu label.
It is fixed.
Hi, thanks for this useful tutorial! One question, how do you generate automatically a json file? do you use svn and a script? Thanks.
Hi Sergio,
Just in PHP
Good, thanks for your reply Misha. So you have a script that generates the json one time or on the fly reading from the readme.txt?
Hello, your article has helped me a lot, but I would like to know how I do that when a new version is detected it is updated automatically, since I have created a special plugin for my clients, and these are not that they have the right knowledge, then it is created a secondary profile where you only have the necessary options as I do to launch that automatic update, or put in the options page that they have to update?
regards
Same for me “I try it in more sites and servers but does not work.. not show “more details” and update message. only the badge “1” on “Plugin” admin menu label.”
WordPress version-> 5.2.1
Thank you for your comment Sam, I will check it out.
same issue is does not show update message only shows badge
It is fixed.
Hi, thanks for this useful tutorial!
I’m using WordPress 5.2.2, I have a problem: after updated my plugin, the wordpress auto add postfix to the plugin folder. Example: my plugin: my-plugin/my-plugin.php, after updated it has been change to: my-plugin-xyz/my-plugin.php
Hi Teo,
Please check your archive folder structure, described in the first part of the tutorial.
Hello Misha, how are you? I did exactly as requested until it appears that it has an update, but the field to update does not appear.
See the attached image.
https://pointcomunicacao.ppg.br/update-plugins/point-pagina-login/print.png
Could my help please?
Resolvido… :)
Perfeito! 🙃
I’m having the same issue. How did you get it the update text to appear?
Bah… I had a typo found in $res->plugin (wrong path)
Thanks for the guide. Everything works great :D
Great 😁💪
Really helpful, after looking around for awhile I am really greatful for this solution. Thanks for the help! Working as of 07 March 2020, WP 5.3.2
You’re welcome! I am happy to help! 🔥
Hi Misha. Great article. I was able to get this to work.
Question and comment.
Q: Why have “misha_update_YOUR_PLUGIN_SLUG” and “misha_upgrade_YOUR_PLUGIN_SLUG” stored? I was able to eliminate the extra wp_options entry by just using one of them.
C: The JSON above was throwing errors at me until I changed “=>” to “:”.
{
“version” => “1.1”, //<-here
"download_url" : "https://rudrastyh.com/misha-plugin.zip",
//…
}
Hello Misha,
thanks for this nice Article.
I got it to work.
I even puted my json file behind a htaccess password protection
But i want to put my zip file behind a htaccess password protection too.
Until now i didnt found a solution.
Do you have any idea?
Hi Misha,
i found a little Bug.
The Function misha_plugin_info should everytime return $res and not false.
If you use the Code in two different Plugins on the same Website, the hook would otherwise override one of the two ‘View details’ Windows and you would get the Message ‘Plugin not found.’ instead.
Comments are closed.