DrPlugin

Update server and releases

Updated October 30, 2026

Publish new versions of the plugins you sell, choose which version customer sites are offered, and deliver each package through a signed download link.

How releases work

Each product keeps a list of releases. A release is one version of your plugin: a version number, a package zip, a changelog and three compatibility fields (Requires WordPress, Tested up to, Requires PHP). A product's first upload is its first release, and every later version is another one.

A release is always in one of three statuses:

Status Meaning
Draft A new upload. No customer site sees it.
Published Live. Update checks can offer it.
Yanked Pulled back. Update checks no longer offer it and its download links stop working, but the release, its changelog and its package stay.

Customer sites are offered the release you published most recently. TierMint refuses to publish a version that is not newer than the current live one unless you override it, so this is normally the highest version.

The update server needs a licensed product with an Update slug. Turn on licensing for a product shows where to set both. On a customer's site, the update client asks your store for new versions.

Managing releases in wp-admin

Open TierMint → Products, open the product and click the Releases tab. You need the manage_tiermint capability. A new product has to be saved once before the tab works.

Upload a release

  1. Under Upload a new release, enter the Version, such as 2.1.0.
  2. Under Package (.zip), choose the plugin zip.
  3. Write the Changelog in Markdown. Write a changelog lists what it supports.
  4. Fill in Requires WordPress, Tested up to and Requires PHP if you use them.
  5. Click Upload release. It stays disabled until you choose a file and enter a version.

The new release appears in the list as a draft. Click Publish on it when you are ready.

The Releases tab with the upload form and a list of a draft release and two published releases

What you can do with a release

The Releases list shows each release with its version, status and package, newest version first. The buttons depend on the status:

Status Buttons
Draft Edit, Publish, Delete
Published Edit, Yank
Yanked Edit, Delete

Yank takes effect at once and does not ask first. Delete asks you to confirm, removes the release and its package, and cannot be undone. A published release cannot be deleted: yank it first.

To publish a yanked release again, use the publish route of the REST API. wp-admin has no button for it.

Publish a version that is not newer

If a draft's version is not higher than the live version, Publish fails with a message that names both versions, and a Publish anyway button appears. Use it only when you mean to put an older version live. It becomes the version customer sites are offered. Sites that already run a higher version see no update.

Check the attached package

Under each version, the list shows the package's download filename, its size and its SHA-256 checksum. The filename is built from the product's update slug and the version, such as orbit-forms-2.0.1.zip. Click the checksum to copy it.

Click Download to get the exact file your customers receive. Use it to confirm that you attached the right build. The checksum is the same value the REST API returns as checksum_sha256.

If the stored file has gone missing, for example because someone deleted it from the uploads folder, the download fails with "The package for this release is missing from storage. Re-upload the build to restore it." Edit the release and attach the package again.

Edit a release after publishing

Click Edit on a release. A draft can change its version, changelog, compatibility fields and package. On a published or yanked release only the version is locked. The changelog, the compatibility fields and the package stay editable. Click Save changes when you are done.

A published release open for editing, with the version locked and the package, changelog and requirements editable

Edits to a live release reach customer sites like this:

  • Changelog and compatibility changes reach a site on its next update check. The update client keeps each answer for up to 12 hours, so the change is not instant.
  • Raising Requires WordPress or Requires PHP stops sites below that version from taking the update. Tested up to only feeds the compatibility note WordPress shows and blocks nothing.
  • Replace package (.zip) points the same version at a new build, and asks you to confirm first. Sites that already updated keep the build they downloaded. Sites that update after the change get the new build under the same version number. If you would rather not have two builds with one version number, yank the release and publish a new version.

TierMint Pro adds release channels and staged rollouts. With Pro you can offer a draft to the sites on a beta or release candidate channel before you publish it, and release to a share of sites at a time. See Release channels and staged rollouts.

Write a changelog

You write the changelog in Markdown. TierMint stores what you type, minus any HTML tags, and turns it into HTML when a customer's site asks for it. The HTML shows on the Changelog tab of the version details window on the customer's Plugins screen.

The WordPress version details window on its Changelog tab, showing a changelog served by a TierMint store

TierMint supports a small part of Markdown:

You type You get
# to ######, a space, then text A heading, level 1 to 6. The space is required.
Lines that start with - or * A bulleted list, one level deep.
Lines that start with 1. A numbered list, one level deep.
A line of three or more backticks, the code, then a closing line A code block, shown as typed.
Other text A paragraph. A single line break becomes a space. A blank line starts a new block.
**bold** Bold text.
*italic* or _italic_ Italic text. An underscore inside a word, as in my_function, stays as typed.
A word in backticks Inline code.
[text](https://example.com) A link. Only http:// and https:// addresses become links. Any other address shows as typed.

Anything else shows as plain text. TierMint removes HTML tags from a changelog when you save the release, so pasted HTML is lost. A lone < character, for example in a code sample, is stored as &lt; and shows to customers as the literal text &lt;, so write such samples without it.

An example changelog:

#### 2.1.0

Adds file uploads and fixes a date bug in Safari.

- File upload field with size limits
- Fixed the date picker in Safari
- `orbit_forms_entry` filter for saved entries

See the [full changelog](https://orbit-plugins.example/changelog) for older versions.

Manage releases with the REST API

Releases live under each product. Every route needs management authentication and the manage_tiermint capability. The licensing REST API page explains how to authenticate with an API key or an Application Password. The Releases tab uses these same routes.

Route What it does
GET /tiermint/v1/products/{id}/releases Lists every release of the product, newest version first, in any status. The list is not paginated.
POST /tiermint/v1/products/{id}/releases Creates a draft release from a multipart upload.
GET /tiermint/v1/products/{id}/releases/{release_id} Returns one release.
PUT or POST /tiermint/v1/products/{id}/releases/{release_id} Edits a release. Use POST when the request carries a new package file.
DELETE /tiermint/v1/products/{id}/releases/{release_id} Deletes a draft or yanked release and its package.
POST /tiermint/v1/products/{id}/releases/{release_id}/publish Publishes a draft or yanked release.
POST /tiermint/v1/products/{id}/releases/{release_id}/yank Yanks a published release.
GET /tiermint/v1/products/{id}/releases/{release_id}/package Downloads the stored package.

A release id that belongs to another product returns 404 tiermint_not_found, the same as one that does not exist.

Create a release

Send a multipart request with the zip in a package file field and the other fields as form fields. version and package are required. changelog, requires_wp, tested_up_to and requires_php are optional.

curl -H "Authorization: Bearer tmk_d9eeedf9827564d7d69c0984b3460e8d:tms_FqzFzDyqlQ57ZcEKieeBBR-N75LV78NaoqVo4cuG5rM" \
  -F "version=2.1.0" \
  -F "changelog=#### 2.1.0
- File upload field with size limits" \
  -F "requires_wp=6.6" \
  -F "tested_up_to=7.1" \
  -F "requires_php=8.1" \
  -F "package=@/path/to/orbit-forms-2.1.0.zip" \
  https://yourstore.example/wp-json/tiermint/v1/products/12/releases

The response is 201:

{
  "id": 87,
  "product_id": 12,
  "version": "2.1.0",
  "status": "draft",
  "has_package": true,
  "package_filename": "orbit-forms-2.1.0.zip",
  "filesize": 184320,
  "checksum_sha256": "f5d20ab37e23ff33cce19ff022dc41d53f85921147a632bb631521ba60e548b6",
  "changelog": "#### 2.1.0\n- File upload field with size limits",
  "requires_wp": "6.6",
  "tested_up_to": "7.1",
  "requires_php": "8.1",
  "released_at": null,
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}

changelog comes back as the stored Markdown, not HTML. The response never says where the file is stored. An extension can add fields to each release with the tiermint_rest_release filter. TierMint Pro uses it to add a distribution object, described on the release channels page.

A request without a package gets 400 tiermint_invalid_params. So does a package that is not a ZIP archive, whatever its file name. When the product has an update slug, TierMint also opens the zip and looks for a PHP file directly inside a folder named after the slug, with Plugin Name: in its first 8 KB. For the update slug orbit-forms, orbit-forms/orbit-forms.php with a normal plugin header passes. A zip without one is refused with 400 tiermint_invalid_params. The check is skipped when PHP's ZipArchive extension is missing on your server.

Version rules

A version has one to four numbers separated by dots, with an optional pre-release suffix: 1.0, 2.1.0, 1.0.0.3 and 2.1.0-beta.1 are all valid. A missing or malformed version gets 400 tiermint_invalid_params.

Each version can exist once per product, whatever its status. Reusing one returns:

{
  "code": "tiermint_duplicate_version",
  "message": "That version already exists for this product.",
  "data": { "status": 400 }
}

Edit a release

Send only the fields you want to change. A draft can change its version, changelog, requires_wp, tested_up_to, requires_php and its package. A published or yanked release can change changelog, requires_wp, tested_up_to, requires_php and its package, but not its version. Sending a different version gets 400 tiermint_invalid_params. Sending the same version counts as no change.

Send PUT /wp-json/tiermint/v1/products/12/releases/87 with this body:

{
  "tested_up_to": "7.2"
}

A new package replaces the old one, and TierMint deletes the old file afterwards. Edit a release after publishing explains what a change to a live release does to customer sites.

Publish, yank or delete a release

POST .../publish makes a release live. It works on a draft or a yanked release and stamps its release date. The version has to be higher than the current live version, or the request gets 400 tiermint_invalid_params with both versions in the message. To publish it anyway, send POST .../publish with this body:

{
  "override": true
}

POST .../yank pulls a published release. Download links already issued for it stop working. Yanking a draft gets 409 tiermint_conflict. A yanked release stays editable, apart from its version, and you can publish it again later.

DELETE removes a draft or yanked release and its package file. A published release returns 400 tiermint_invalid_params: yank it first. The response confirms what was removed:

{
  "deleted": true,
  "previous": { "id": 87, "version": "2.1.0", "status": "yanked" }
}

The previous object holds every field of the deleted release. It is shortened here.

When two requests change the same release at once, the slower one gets 409 tiermint_conflict. Reload the release and try again.

What customer sites receive

A customer's site asks your store for updates with GET /wp-json/tiermint/v1/update-check. The update client sends this request for you. The route is public and takes these query parameters:

Parameter Required Meaning
product_slug Yes The product's catalog slug, the same one the licensing routes use.
license_key Yes The customer's license key.
site_url No The site's URL. Without it there is no download link.
installed_version No The version installed on the site. Without it, update_available is true whenever a release is live.

The route passes every argument it receives to the tiermint_update_check_release filter as params. An update client may also send a channel argument. The free plugin ignores it, and an extension can read it from params. Developer hooks describes the filter.

For a valid license on a site that runs an older version, the answer carries the release details, its changelog as HTML and a signed download link:

{
  "name": "Orbit Forms",
  "slug": "orbit-forms",
  "new_version": "2.1.0",
  "requires": "6.6",
  "tested": "7.1",
  "requires_php": "8.1",
  "last_updated": "2026-10-13T10:00:00Z",
  "update_available": true,
  "sections": { "changelog": "<h4>2.1.0</h4><ul><li>File upload field with size limits</li></ul>" },
  "license": { "valid": true, "status": "active" },
  "package": "https://yourstore.example/wp-json/tiermint/v1/package/download?product=12&release=87&license_key_id=345&site_hash=9c1e...&expires=1791900300&signature=..."
}

slug is the product's update slug, or its catalog slug when it has none. TierMint issues the download link for any site that sends a valid license key and a site_url. The site does not need an activation.

An expired, disabled or unknown license still sees the new version and its changelog, because WordPress needs them to list the update. The answer has no package link, and it adds an upgrade_notice: "Your license has expired. To receive this update, buy a new license and use its key." for an expired license, or "A valid license is required to receive this update." otherwise. The route answers 200 in these cases.

When nothing is published for the product yet, the answer carries name, slug, "update_available": false and the license block, with no version or package. An unknown product_slug returns 404 tiermint_not_found. So does a draft or archived product, unless the key is valid for it, so a product you have not published does not show its version or changelog. An archived product keeps serving the sites whose licenses are still valid.

This route shares the licensing rate limits: 60 requests an hour per license key and 300 an hour per IP address. The update client caches each answer for 12 hours, so a normal site stays far below them.

TierMint Pro reads the channel argument and offers beta and release candidate drafts to the sites that ask for them. The release channels page explains how.

Signed package downloads

Customers never download a package file directly. Every download goes through a signed link that carries the product id, the release id, the license's id, a hash of the site it was issued to and an expiry time, plus a signature over all of them. The license key itself is never in the link, so it does not end up in server logs or browser history.

  • A link from an update check works for five minutes. WordPress downloads the package right after it asks, so this is enough. Developers can change the time with the tiermint_download_url_ttl filter, down to 60 seconds.
  • TierMint checks the license again when the download starts. If the license expired or was disabled after the link was issued, the download is refused.
  • Yanking a release stops every link already issued for it, even links that have not expired. Yanked is the only release status that stops a download.
  • On its own, the free plugin signs links for published releases only.

A refused download returns one of these:

Code Status Meaning
tiermint_invalid_signature 403 The link was changed or is incomplete.
tiermint_expired_url 403 The link is genuine but its time is up. The update client fetches a fresh one.
tiermint_forbidden 403 The link is genuine, but the license is not active for that product or the release was yanked.
tiermint_not_found 404 The release does not exist under that product.
tiermint_server_error 500 The package file is missing from storage.

Protect the packages folder on NGINX

TierMint stores package files in wp-content/uploads/tiermint/packages/, in a random folder per file. It adds .htaccess, web.config and index.php files that block direct access on Apache and IIS, and it never links to the folder.

NGINX does not read .htaccess files. On NGINX, add this rule to your site's server block, then reload NGINX: location ^~ /wp-content/uploads/tiermint/packages/ { deny all; }

Downloads in the customer account

Customers also get files from the Downloads tab of their customer dashboard. It lists the product files from their completed and partially refunded orders. Under Updates, it lists the newest published version of each product they hold an active license for, with a Download update link.

The Downloads tab with a purchased file under one product and an update under another, with its version and download link

The links on this tab work for six hours after the page loads, and TierMint checks the order or license again when one is used. Developers can change the time with the tiermint_download_url_ttl filter. A full refund removes the order's files, the same way it disables the order's licenses.

Logged-in customers can also list their files with GET /wp-json/tiermint/v1/me/downloads. Each entry carries a fresh signed link.

Developer hooks

Two filters let an extension change what the update server returns. TierMint Pro uses both.

Filter What it changes
tiermint_update_check_release The release an update check serves.
tiermint_rest_release The fields of each release in the releases REST responses.

tiermint_update_check_release

The filter receives the release the check would serve, which is the newest published release or null, and a context array. Return any release of the product that is not yanked, or null for the no-update answer. The context array holds these keys:

Key What it holds
product The product.
params The request's arguments as received, such as channel.
site_hash A stable hash of the site URL, or null.
installed_version The version the site reports, or null.
result The license validation result.

params is raw request input and holds the license key. Sanitize what you read from it and do not log it.

add_filter(
	'tiermint_update_check_release',
	function ( $release, $context ) {
		$channel = isset( $context['params']['channel'] ) && is_string( $context['params']['channel'] )
			? sanitize_key( $context['params']['channel'] )
			: '';

		// Pick another release for $channel here, or return $release as it is.
		return $release;
	},
	10,
	2
);

tiermint_rest_release

The filter runs for every release in the releases REST responses. It receives the release fields as an array and the DrPlugin\WP\TierMint\Updates\Release object. Add your fields to the array and return it. A return value that is not an array keeps the fields TierMint built.

Common questions

I tried to publish a lower version by mistake. What happened?

Nothing went live. TierMint refused the publish because the version is not higher than the live one. To publish it anyway, use Publish anyway, or send "override": true to the publish route.

A customer does not see an update I just published. What should I check?

First check that the release is Published, not still a draft. Then allow for caching: the update client keeps each answer for 12 hours, and WordPress has its own update schedule. A site that checked recently picks up the new version when its cache runs out.

Can I change a changelog after publishing?

Yes. Only the version is locked once a release is published or yanked. The changelog, the compatibility fields and the package stay editable.

A release has a serious bug. How do I pull it?

Click Yank. Update checks stop offering it at once, and download links already issued for it stop working. Fix the bug and publish a new, higher version. If you need the yanked version live again in the meantime, publish it through the REST API with "override": true.

Does yanking or deleting a release remove it from customer sites?

No. It only changes what future update checks and downloads can see. A site that already installed the version keeps running it.