Library / SDK
YahnisElsts/plugin-update-checker avatar
YahnisElsts/plugin-update-checker

plugin-update-checker: the field reference is a spreadsheet, and three sections share a heading

A custom update checker for WordPress plugins. Useful if you don't want to host your project in the official WP repository, but would still like it to support automatic updates. Despite the name, it also works with themes.

2,579 stars436 forksPHPMIT

At a glance

What is it?
A PHP library that gives self-hosted WordPress plugins and themes the automatic update screen they would have if they lived in the official repository. The advice that matters most is an italic note about hook timing, and the advice that would bite you is a slug derived from a filename.
Who is it for?
Judgment: this is infrastructure rather than a product, and it is the right answer for anyone shipping a commercial plugin who does not want to open a WordPress.org account. Two details in the file are worth more than the rest.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • YahnisElsts/plugin-update-checker2,579 ★ · +12
Star history
Repository2026-10-012026-11-14
YahnisElsts/plugin-update-checker2.6k2.6k

12 Stars in the last 44 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The full field list lives in a spreadsheet, not in the repository

The plugin metadata file is deliberately shown in minimal form:

json
{
	"name" : "Plugin Name",
	"version" : "2.0",
	"download_url" : "https://example.com/plugin-name-2.0.zip",
	"sections" : {
		"description" : "Plugin description here. You can use HTML."
	}
}

Four fields, and then a sentence saying this leaves out optional ones, with a link to a shared spreadsheet for the complete list and descriptions of every supported field.

That spreadsheet is the schema. It is the authoritative description of what this library will read out of your JSON, it is not in version control, and it is not reviewed in a pull request when it changes. If you are shipping a commercial plugin, the useful defensive move is to copy the fields you rely on into your own repository as a fixture, so a change upstream is a diff you can see rather than a release note you have to remember.

The theme example that follows it is called out as actually being complete, showing all theme-related fields, and it adds one the plugin example omits: a details URL, which is the page a user sees when they click through an update notification.

Both examples also show the version as a string rather than a number, which matters because the library compares them as strings.

The two JSON examples in one file use two different formatting styles

Compare the two blocks above and below, because they are formatted differently in the same file.

The plugin example puts a space on each side of the colon: name, version, download_url and sections are all written with a space before and after the separator. The theme example, immediately after it, uses the conventional tight form with no leading space.

json
{
	"version": "2.0",
	"details_url": "https://example.com/version-2.0-details.html",
	"download_url": "https://example.com/example-theme-2.0.zip"
}

Neither is wrong JSON, and both parse. But any editor configured to reformat JSON on save will rewrite the first one and leave the second alone, which means a copy-paste from one example and a commit later produces a diff that touches every line for no reason.

There is a related detail worth knowing. These files live in an `examples` directory inside the library, and the setup instructions tell you to copy the whole library directory into your plugin, then open the example file in that directory and replace its placeholder data. So the directory you ship into production contains an examples folder holding two JSON files full of someone else's placeholder data, unless you delete it.

Hook timing decides which tools can see your updates

This is the most consequential sentence in the file and it is set in italics as a note above the setup steps.

Creating the checker during the plugins_loaded action, or outside any hook at all, is the recommendation. Creating it only during an admin-style action is what causes trouble, and the reason is not about your own dashboard: updates will not be visible to a wide variety of WordPress management tools. They will only be visible to logged-in users on dashboard pages.

In practice that means the update notice never reaches anything that is not a person looking at a browser page. Tools that enumerate plugin updates programmatically, and anything that acts on them, see nothing, so an update that is available never gets surfaced or installed by the thing you were counting on.

The wiring itself is four lines of PHP, and the library namespaced under its own vendor:

php
require 'path/to/plugin-update-checker/plugin-update-checker.php';
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;

$myUpdateChecker = PucFactory::buildUpdateChecker(
	'https://example.com/path/to/details.json',
	__FILE__, //Full path to the main plugin file or functions.php.
	'unique-plugin-or-theme-slug'
);

Note also the third argument, and the note that if you are using the Composer autoloader you do not need the explicit require at all. Two loading paths, and the version appears in the namespace.

The slug is derived from your filename, so two plugin.php files collide

The slug argument is described as optional but recommended, and the notes section explains what happens if you leave it out.

Without a slug, the checker uses the name of the main plugin file, so a file called `my-cool-plugin.php` yields the slug `my-cool-plugin`. The file says plainly that this can lead to conflicts if your plugin has a generic file name like `plugin.php`.

So two unrelated plugins, each with a `plugin.php` at its root and neither passing a slug, end up with the same identifier. The same section then explains why that matters beyond tidiness: if you plan to use the slug in your own code, for filtering updates or overriding checker behaviour, it is a good idea to set it explicitly.

In other words the slug is a public interface that third-party code filters on, and its default value is derived from a filename you did not choose to be generic. A collision is not a cosmetic duplicate; it means a filter registered for one plugin is evaluated for the other.

There is an asymmetry worth noting. This does not affect themes, because the theme directory name is used as the default slug instead. Themes get a stable, directory-derived identifier for free; plugins have to be told what theirs is.

Themes have no check button, so the workaround is a separate debug plugin

The polling interval is the first useful number in the file: by default the library checks the specified URL for changes every 12 hours.

To override that, there is a link. Clicking Check for updates on the Plugins page forces an immediate check, and the file helpfully says where to find it: next to the Visit plugin site link.

Then the sentence that matters: themes don't have that link.

The documented alternative is four steps, and every one of them involves another plugin. Install a debugging toolbar. Open its menu in the admin bar. Find the panel named for your slug. Click Check Now.

So for a theme author there is no first-class way to trigger a check, and the file offers a developer tool instead. That panel is also the clearest statement of how the slug is used: the slug is the panel identifier, so a collision between two `plugin.php` files would also merge their panels.

Releasing an update is the other half of the loop. Change the version number in the JSON file, make sure the download URL points at the latest version, and update the other fields if needed, with a companion project offered for automating that step.

A private repository means the access token is a string in your source

The GitHub integration differs from the self-hosted one in one argument: the first is a repository URL rather than a JSON file URL. Two method calls then configure it.

One sets the branch that contains the stable release, so the checker does not treat every commit on the default branch as a version. That is the setting that stops a development branch from being offered to users as an upgrade.

The other is optional and is for private repositories, and it takes the access token directly:

php
$myUpdateChecker->setAuthentication('your-token-here');

A literal string in the plugin source. The library needs the token at request time to read a private repository, and this is the API it offers, which means for a distributed plugin or theme the token ends up in a PHP file that ships with it. On a site you control that is a configuration detail; in a plugin you hand to a customer it is a credential in your codebase.

If your repository is public you do not need this call at all, which is the configuration the file presents as the norm and this call as the exception.

One other detail for plugins only: a readme file formatted to the WordPress.org plugin readme standard is what gets shown when a user clicks through to see version details, so that file has to exist in your repository even though you are not on WordPress.org.

Three sections share a heading and the versioned namespace is major-only

The table of contents at the top is machine generated, between two HTML comments that say to keep them and to re-run the generator rather than edit the section. That is good practice, and it has left visible fingerprints.

How to Release an Update appears three times, once for the self-hosted flow, once for GitHub, once for BitBucket, and the generator disambiguated them into anchors ending in nothing, then a dash and one, then a dash and two. Notes appears twice with the same treatment. So three of the document's headings are identical strings and the links can only be told apart by an auto-generated suffix.

The fourth integration section avoids the problem by being worded differently: the GitLab one is titled How to Release a GitLab Update rather than How to Release an Update. So the file has four near-identical instructions, three with the same heading and one with a different one.

The BitBucket spelling also carries an internal capital on the second syllable throughout, which is not how the service writes its own name.

Underneath the documentation, the versioning has its own seam. The namespace is versioned by major only, and the loader file at the root is pinned to a minor, `load-v5p7.php`, alongside a shim named after the library and the namespaced source directory. Releases are tagged with a v prefix and named without one. So a 5.8 release adds a loader file while keeping the same namespace, and anyone who pinned the loader filename rather than the library directory has to notice that.

Editorial conclusion

Judgment: this is infrastructure rather than a product, and it is the right answer for anyone shipping a commercial plugin who does not want to open a WordPress.org account. Two details in the file are worth more than the rest. First, hook timing: create the checker on `plugins_loaded` or outside any hook, because constructing it only inside an `admin_*` action hides your updates from every third-party management tool. Second, the slug: set it explicitly, because the fallback derives it from the filename and two plugins both called `plugin.php` will collide, and that slug is the identifier other code filters on. Three things to verify before you ship. The full field list for the JSON metadata lives in a linked spreadsheet, so pin a copy of the schema in your own repository. A private repository means the access token is a string literal in your plugin source, which is fine for a site you control and not fine for a plugin you distribute. And if you are updating a theme rather than a plugin, there is no manual check button in the interface, so the documented path is installing a separate debugging plugin.

Frequently asked questions

What is the plugin update checker library for WordPress?

It is a PHP library that adds automatic update notifications and one-click upgrades to self-hosted WordPress plugins and themes, using the default WordPress upgrade interface. You describe your release in a JSON file, host it somewhere publicly reachable, and pass its URL to the library, which polls it and shows an update notice. Despite the name it works with themes as well as plugins.

How do I set up automatic updates for a self-hosted WordPress plugin?

Copy the plugin-update-checker directory into your plugin, open the example JSON in its examples subdirectory, replace the placeholders with your name, version and download URL, upload that file somewhere public, then call the update checker factory with the JSON URL, the absolute path to your main plugin file, and a unique slug. Create it on the `plugins_loaded` action or outside any hook, so updates stay visible to third-party management tools.

How often does the update checker look for new versions?

By default it checks the configured URL every 12 hours. You can force an immediate check with the Check for updates link on the Plugins page, next to the Visit plugin site link. Themes have no such link, so the documented route is installing a debugging toolbar, opening the panel named for your slug, and clicking Check Now.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. YahnisElsts/plugin-update-checker on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/yahniselsts-plugin-update-checker.svg)](https://hysenlabs.com/projects/yahniselsts-plugin-update-checker)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/yahniselsts-plugin-update-checker.svg)](https://hysenlabs.com/projects/yahniselsts-plugin-update-checker)

Was this useful?