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
- Under Upload a new release, enter the Version, such as
2.1.0. - Under Package (.zip), choose the plugin zip.
- Write the Changelog in Markdown. Write a changelog lists what it supports.
- Fill in Requires WordPress, Tested up to and Requires PHP if you use them.
- 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.

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.

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.

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 < and shows to customers as the literal text <, 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_ttlfilter, 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 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.