Offline and team licenses
Updated October 24, 2026
Sign license answers your plugins can check offline, activate sites with no internet access, and sell team licenses with seats. This needs TierMint Pro.
TierMint Pro adds signed license answers, offline activation by file and team licenses to the free licensing engine. With Pro inactive, license answers carry no signature and the public key route does not exist. Member keys keep validating, as plain licenses. New purchases stop being team licenses, and the Team panel and the seat routes go away. If you deactivate Pro has the details.
Signed license answers
With Pro active, every response from /license/validate and /license/activate carries a signature block. Your plugin can check that block against a public key your store publishes. A valid signature proves the answer came from your store, even when something between the customer's site and your store could change the reply. The store signs an activate answer only when the activation succeeds. It signs a validate answer unless the status is invalid or key_mismatch. Error answers carry no signature.
Your store creates an Ed25519 key pair the first time it needs one. The secret half stays encrypted on your server. The public half is served without authentication at GET /wp-json/tiermint/v1/public-key, and the reply looks like this:
{
"alg": "ed25519",
"active_key_id": "a1b2c3d4e5f60718",
"keys": { "a1b2c3d4e5f60718": "BASE64_PUBLIC_KEY==" }
}
There is no control to rotate the key. The key changes only when the store cannot decrypt the stored secret, for example after the SECURE_AUTH_KEY changes, and the store then creates a new key without any notice. It keeps serving the old public keys, so earlier answers still verify. Fetch the public keys again when reason is unknown_key.
Check a signature in your plugin
TierMint has a single-file, dependency-free verifier, tiermint-license-verifier.php, with the class TierMint_License_Verifier. It needs PHP 7.4 or newer with the sodium extension. It is not part of the installed TierMint plugin. It comes in tiermint-client.zip, together with the update client. Add it to your own plugin.
- Fetch the public key once from the
/public-keyroute. - Add the verifier file to your plugin and pin the key in the constructor.
- Pass each validate or activate response to
verify_response().
require __DIR__ . '/vendor/tiermint-license-verifier.php';
$verifier = new TierMint_License_Verifier(
array( 'a1b2c3d4e5f60718' => 'BASE64_PUBLIC_KEY==' ),
7 * DAY_IN_SECONDS // Reject an answer older than this.
);
$result = $verifier->verify_response( $response, get_option( 'my_license_key' ), home_url() );
if ( $result['verified'] && 'active' === $result['status'] && 'my-product' === $result['product_slug'] && $result['site_bound'] ) {
// Signed by your store, for this key, product and site address, recently enough to trust.
}
A signature proves who signed an answer, not when. So the verifier also checks that the answer was made for this license key and this site, and that it is no older than the maximum age you pass. The default is seven days, and 0 turns the age check off. Pick the age to match how fast a refunded or revoked license should stop working. For a lifetime license with no expiry date, the age is the only limit.
Compare product_slug in the result, as the example does. The signed answer always names the key's own product, so this check is what ties the key to your product. If you also send product_slug on the validate call and it does not match, the store answers key_mismatch with no signature, and the verifier reports malformed_signature.
site_bound is true when the answer was made for a site address. It does not show that the site holds an activation slot: a validate answer is signed for whatever site URL the caller sent, and the site_active field is not signed. To rely on a slot, check the signed answer to an activate call or the offline response file. The store signs those only after the site is activated.
When verified is false, reason says why:
| Reason | Meaning |
|---|---|
bad_signature |
The signature does not match. The answer did not come from your store, or it was changed. |
unknown_key |
The answer was signed with a key you have not pinned. Fetch the public keys again. |
key_mismatch |
The answer was made for a different license key. |
site_mismatch |
The answer was made for a different site. |
stale |
The answer is older than the maximum age. |
malformed_signature, malformed_payload |
The signature block or its contents could not be read. |
sodium_unavailable |
The site's PHP has no sodium extension, so nothing can be verified. |
The file defines the class only when no other copy has defined it, so when several plugins on one site bundle it, the first copy loaded wins. To be sure your plugin uses its own copy, rename the class to your own prefix.
Offline activation
Some customers run sites that cannot reach your store at all. Offline activation lets them activate a license by carrying two files between the offline site and a computer that is online.
- On the offline site, your plugin creates a request file with the license key and the site address.
- The customer copies the file to a computer that is online, signs in to your store and opens the Offline activation panel on the account dashboard.
- They choose the file under Activation request file and click Activate offline.
- The store activates the site and offers a signed
.tiermint-licensefile to download. - The customer copies that file to the offline site and imports it. Your plugin verifies it with the pinned public key, with no network call.
The panel lists the customer's licenses with their status and how many of their sites are in use.

An offline activation uses one activation slot, the same as an online one. A site whose address counts as local or staging, such as one ending in .test or starting with staging., uses no slot, under the same rules as online activations in the licensing guide. Uploading the same request file again gives a new response file for the same site and uses no second slot.
The store refuses the file in these cases:
| Case | Message |
|---|---|
| The key does not belong to the signed-in customer | No license you own matches that request file. |
| Every slot on the license is in use | This license key has reached its activation limit (N sites). |
| The license has expired | This license has expired. To activate new sites, buy a new license and use its key. |
| The license is disabled | This license has been disabled. |
To free a slot, the customer deactivates a site from their licenses on the account dashboard, then uploads the file again.
There is no offline deactivation file. An offline site shows in the customer's licenses like any other site, and the customer or you can deactivate it there to free the slot. The store cannot reach the offline site, so that site is not told.
Build the request and read the response
The request file is JSON, plain or base64 encoded, with license_key, site_url and an optional fingerprint:
{
"license_key": "YOUR-LICENSE-KEY",
"site_url": "https://intranet.example.com",
"fingerprint": ""
}
The response file is base64-encoded JSON with type set to tiermint_offline_activation, version, site_url, expires_at and a signature block. The block is signed the same way as an online answer, so pass it to the verifier's verify() method with the license key and the site URL. If the request file had a fingerprint, pass it as the fourth argument, or the result is site_mismatch. The age check applies here too, so allow for the time the file takes to reach the offline site.
Team licenses and seats
A team license is one license that a buyer owns and that issues a seat for each team member. A seat is a separate license of the buyer, with its own key. It copies the team license's site limit and counts its sites on its own. Pro records which team license each seat belongs to in its own tables, not in the free plugin's. The buyer manages all the seats from one place, and members do not need an account on your store.
Make a product a team product
Give the product a seat_limit setting: the number of seats each purchase grants. Licenses issued for the product after that are team licenses with that many seats. The product editor has no field for it, so set it through the REST API. The products routes need management authentication and the manage_tiermint capability, as the licensing REST API guide explains.
- Send
GET /wp-json/tiermint/v1/products/{id}and copy thesettingsobject from the reply. - Add
seat_limitto that object. - Send
PUT /wp-json/tiermint/v1/products/{id}with the whole object.
{
"settings": {
"seat_limit": 10
}
}
The seat count comes from the product, so a customer cannot give themselves seats. A product without seat_limit sells ordinary licenses.
Manage seats
The buyer sees a Team panel on the account dashboard. For each team license it shows the seats in use, such as "2 of 3 seats used", and each seat's member key, status and site count.

Add a seat creates a new member key, up to the seat limit. It does not check the status of the team license, so on a disabled, refunded or expired team license with a free seat it still creates an active key. At the limit, the panel says "This license has reached its seat limit (N seats). Revoke a seat to free one."
Revoke turns that member's key off at once and frees the seat, after the buyer confirms. A revoked seat cannot be turned back on. Add a new seat instead.
Seats do not show in the buyer's own license lists: the Licenses tab, the Updates section of the Downloads tab and the Offline activation panel. The Team panel is the one place that lists them.
The buyer's personal data export (Tools → Export Personal Data) lists every seat key with their other licenses. An erasure request disables all of them.
What happens to seats when the team license changes
Each seat is its own license. While TierMint Pro is active, it keeps the seats in step with the team license:
| Team license event | What happens to the seats |
|---|---|
| Renewed | Every seat that is not disabled moves to the new expiry date. An expired seat becomes active again. |
| Expired | Active seats expire with it. |
| Disabled or refunded | Every seat is disabled. |
| Enabled again | Every disabled seat that was not revoked comes back, including a seat disabled on its own. Seats the buyer revoked stay off. |
A member's key validates like any other license key, so seats add no extra work to a license check.
If you deactivate Pro
Deactivating Pro changes nothing in your data. The team records stay in Pro's tables, and every seat license stays as it is.
While Pro is off:
- Each seat is a plain license of the buyer. It keeps working until its own expiry date.
- Each seat shows in the buyer's Licenses tab and in the Updates section of the Downloads tab, like any other license.
- The Team panel and the seat routes are not available.
- Renewing, disabling, refunding or expiring the team license does not reach the seats.
The table above applies again from the next change to the team license after you activate Pro. Pro does not replay changes made while it was off.
Deleting Pro with Delete all data on uninstall on removes the team records with Pro's other data. The licenses themselves belong to the free plugin, so they stay.