Automatic Updates for Plugins and Themes Hosted Outside WordPress Extend

Here are two sample scripts along with an API to provide automatic updates for plugins and themes you host on your own server.

Inside /api you’ll find index.php which processes all the update requests. You should place this in something like and update $api_url in /plugin/test-plugin-update/test-plugin-update.php and /theme/portfolio-racer/inc/updates.php accordingly. If you activate these sample plugins without changing API URL, updates will be checked against my test server. If you decide to update, both plugin and theme will be replaced with exactly the same version of each.


  1. Ivan Novak says:

    Hey there,
    Quite the awesome idea but I can’t seem to get it to work when I change the API url to the correct path on my own server.

    I did some investigation and found that if I print_r($raw_response); after line 44 in the test-plugin-update.php file, which is the activated plugin, I’m getting an error on line 41 of the index.php file in the api folder. The error is as follows:

    [body] =>
    Warning:  array_shift() [function.array-shift]: The argument should be an array in /home/ivannova/public_html/demo/wpdev/api/index.php on line 41

    Any ideas would be welcome. Thanks!

  2. Kaspars says:

    It looks like $packages[$args->slug]['versions'] is not an array. You could check if $packages[$args->slug] is an array before selecting $latest_package.

  3. Ivan Novak says:

    Thanks for the reply! I added:

    if (is_array($packages[$args->slug]))
        $latest_package = array_shift($packages[$args->slug]['versions']);

    And now the plugin reports that it is, in fact, out of date as expected. However, I now receive the message:

    There is a new version of Test Plugin Update available. View version Details automatic upgrade unavailable for this plugin.


    • Kaspars says:

      Ivan, I really can’t debug it without direct access to the code. If you are going to implement this into any of your themes/plugins, this might be a good opportunity to take the time and learn how the update procedure works.

  4. Ivan Novak says:

    Kaspars, thanks for your assistance in getting me on the right path to solving the problem. It is truly appreciated. The problem ended up being that the wp_remote_post(); function actually adds slashes within the $_POST['request'] serialized array. A simple addition of stripslashes() around $_POST['reqeust'] around line 36 does the trick, like so:

    $args = unserialize(stripslashes($_POST['request']));

    All the best.

  5. David Gwyer says:

    Just a quick(ish) couple of questions.

    Does the api index.php file work by informing WordPress that an update is necessary if the date OR the version are different (or both different)? And does it make a difference what the actual date is of the new version (zip file), i.e. is it just the date specified in the index.php api page that rules whether an update is necessary.

    Also, if I wanted to update just by version number and date, is this easy to do?

  6. David Gwyer says:

    I’m not sure I follow.

    So the actual date of the Plugin/theme zip file, and date entered into the $packages array, never cause an update to occur? And it is purely the version number entered in the api index.php that triggers the update?

    If I wanted to trigger an update do I just change the version number in index.php, and that’s it?

    Great script by the way.

    • kaspars says:

      Correct, David — the date is used only for refference, so that both you and the user know when the update was released. In the API index.php you see a version comparison, which triggers the actual update:

      if (version_compare($args->version, $latest_package['version'], '<'))
      	$update_info->new_version = $update_info->version;
  7. Robin says:

    I can’t for the life of me get the theme updater to achknowledge an update. It’s a shame because that is the only part I want. This class does a good job of the plugins. If only it support themes too or if I could get this one to work :P

    Are there any special requirements that might not be obvious?

  8. Robin says:

    Hi kaspars,

    yep I enabled that. I’ve tried on my local host and on a remote server. I even tried just uploading the unmodified racer theme to one of my sites and it still didn’t do anything even with forced requests on :P

  9. Seth Merrick says:

    Hey kaspars,

    Thank you so much for making this available!

    I’m currently using the code in both a theme and a plugin with great success.

    I’m working on a new plugin that will auto-update, and it’s raised one question – do you know what I need to add to the $package returned by the API in order to change the message inside WordPress > Updates from “Compatibility with WordPress 3.0.5: Unknown” to “Compatibility with WordPress 3.0.5: 100% (according to its author)”??

    My plugin is confirmed to work up through the latest development trunk of WP, and it’s a shame to see the Update page say that compatibility is “Unknown”

    Thanks again for everything!

  10. p.s. don’t copy and paste the code from my earlier comment. It looks like WP cropped some of the longer lines.

  11. Don says:

    Seth Merrick,

    Simply add a ‘tested’ value to the $packages array, like so:

    $packages['your-plugin'] = array(
           'versions' => array(
                   'x.x' => array(
                           'tested' => '3.1.1', // highest version of WP your plugin works with

    And then, reflect that change in the ‘plugin_information’ check further down in the document:

    if ($action == 'plugin_information') {
                   $data = new stdClass;
                   $data->tested = $latest_package['tested']; // pull in the value from $packages

    That’s it. Worked for me anyway.

    Good luck!

  12. Dimas says:

    Important: if you want to use this system to update multiple plugins (on the same environment) … the “plugins_api” filter should never at any point return “false”. Instead, based off of the args it receives it needs to lookup and return a result.

    Additionally consider using a hash/assoc-array when calling the wp_remote_post() function … something like the following, this will ensure that a slug is directly tied to a api_url.

    $request = wp_remote_post($hosts[$args->slug], $request_string);

    The plugins_api has a weird implementation, see /wp-admin/includes/plugin-install.php … plugins_api() function … note how when the filters are applied, it needs to return a result. Having an implementation which returns false some of the time and a result other times causes problems here.

  13. Thank you Kaspars, thank you others in the comment thread. Useful stuff, got it working for my plugin in no time.

  14. Daniel says:

    That’s really great. I got it working but I have a question:

    I would like to protect the package url. Now it is a direct link to a zip file but I would like to point to a download.php file. So, my question is, how the file content (of the zip file) has to be served from the download.php file that wordpress can install it?

    Best regards,

    • Kaspars says:

      Daniel, here is how I would do it — store all your client domain names and use them as identifiers (set as an HTTP referrer) when an update request is made. Once the request hits your download.php, simply check if that domain is in your database. Yes, this approach is not 100% reliable as that header can be spoofed, but it’s a very simple and fast way of doing it.

  15. Valentin says:

    To make it work with my theme on WordPress 3.3.1 I had to apply Ivan’s fix and change 2 lines of code to this:

    $theme_base = basename(get_template_directory());
    $ver = $checked_data->checked[$theme_base];

    $theme_base/$theme_base.php, but just simple $theme_base.

    Hope it helps.

  16. RonakG says:

    Amazing script. Thanks Kaspars. I got it working with some struggle. First I was not getting the slug on my server, so I had to hard-code it in the script. Then I changed the packages array to add lot more information so that it feels like the update is coming from the WordPress server (now it also generates that Details pop-up when a user clicks the link – get version x.x details).

    Only thing I’m not able to figure out is the Upgrade Notice. Is there a way to get that working?

  17. RonakG says:

    Ivan Novak, I also faced this same issue and the stripslashes() fixed it. Thanks for the same. I wouldn’t have figured it out myself.

    Also, I could figure out about the upgrade notice part. Just add a field called ‘upgrade_notice’ in the response.

  18. Weemo says:

    I really appreciate you sharing such a great solution. I’m having an issue with the versioning of my theme. No matter what version I’ve identified in the styles.css, the script always displays that there is an upgrade, even if the theme is a newer version that identified in the api/index.php. Not sure what I’m doing wrong.

  19. RonakG says:

    Hey Guys,

    I took this script and made some changes so that management is little easier. You can take a look at it here –

  20. Weemo says:

    Ah, Perfect. Thanks for the link.

  21. Daniel says:

    Just wondering if anyone managed to get the theme api working and if so can you please share any updates that you may have made?

  22. Todd Lahman says:

    There is this commercial Plugin and Theme Update API manager for WooCommerce.

  23. Joel James says:

    OMG !! This API rocks!! Thank you so much Kaspers :) I was searching for a solution like this. Found many paid services that I can not afford. Thank you so much.

  24. Amit says:

    Hello Kapars,
    Even after 5 years this script works like charm.
    but i got a question i protected archive folders with basic access authentication with htaceess. i do have stored logins in my .htpasswd. and i am able to get update details from secured folder by adding this header in wp_remote_post

    'headers' => array(
              'Authorization' => 'Basic ' . base64_encode(sprintf('%s:%s',
     'username', 'password'))

    But when click to update it comes with
    Download failed. Authorization Required

    i am not sure where its lacking, can you help ?

    Thank you very much.

    • Kaspars says:

      Amit, I think your approach doesn’t actually protect the archive folder because anyone with access to your source code will be able to extract the base64 encoded authorization header.

      Secondly, you won’t be able to change those credentials at a later date because you’ll break the update functionality for everyone using the old credentials.

      Maybe I misunderstood you question, so please feel free to post a followup question.

  25. If you’re looking for a service that manages this for you check out It provides automatic WordPress plugin & theme updates, license management, git deployment, and more.

Leave a Reply