Use cases
Offer a free trial
Let customers self-serve a time-limited trial key from your own site — Keyright mints it, emails it, and your app activates it exactly like a paid key — or hand an air-gapped evaluator a signed offline trial file.
Goal
Let a prospect try your product before buying, with no work on your side: they click Start free trial on your marketing site, Keyright mints a time-limited trial license, emails them the key, and your app activates it exactly like a paid key. When the trial expires the app falls back to its unlicensed state, and buying a real key supersedes the trial with no reinstall.
Throughout, $BASE is your issuing service URL and $TOKEN is a tenant admin token — see Getting started.
When to use it
- Use this if you want self-service evaluation driven from your own site, one trial per email per product, with the clock starting when the customer activates.
- Use this if you have an air-gapped or enterprise evaluator — hand them a signed offline trial file instead (below), no network required.
- Don’t use this to extend or convert a paid customer — renewals and upgrades are vendor-side changes to an existing license (see Activate, deactivate & move seats). Trials are off by default; a product offers them only once you set a trial length.
What you do (vendor side)
1. Enable trials on a product
A trial is defined by three things: days (length; <= 0 disables), tier (the feature tier the trial grants), and seats.
Dashboard: open the Products tab and set the trial length, tier and seats on the product.
pro) and seats (1) — and confirms anyone can self-serve one with a work email.API — set them on an existing product:
curl -X POST $BASE/admin/products/nebula/trial -H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
-d '{"days":30,"tier":"enterprise","seats":1}'
# -> { "product":"nebula", "trialDays":30, "trialTier":"enterprise", "trialSeats":1 }
Or set them when creating the product (trialDays, trialTier, trialSeats on POST /admin/products):
curl -X POST $BASE/admin/products -H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
-d '{"name":"Nebula","slug":"nebula","trialDays":30,"trialTier":"enterprise","trialSeats":1}'
GET /admin/products returns each product’s trialDays/trialTier/trialSeats. Set days to 0 to turn trials back off.
2. Let a customer request a trial
POST /v1/trial is public (no auth) and CORS-open, so a browser form on your marketing site can post to it directly:
curl -X POST $BASE/v1/trial -H "content-type: application/json" \
-d '{"product":"nebula","email":"dev@acme.com","company":"Acme"}'
{
"ok": true,
"product": "nebula",
"trial": true,
"emailed": true,
"expiresUtc": "2026-10-23T12:00:00.0000000Z",
"days": 30,
"created": true
}
The response confirms the trial was issued and emailed — it deliberately does not contain the key (no key, tier, or seats field). Keyright emails the trial key to the address; the customer then enters it into your app, where it activates exactly like a paid key.
| Field | Required | Notes |
|---|---|---|
product | yes | the product slug |
email | yes | the trial is keyed to this address |
company | no | becomes the licensee name; defaults to the email |
tenant | no | only needed if the same slug exists in more than one tenant, to disambiguate |
One trial per email per product. Re-requesting with the same email returns the same key (created:false) rather than minting a new one — idempotent and abuse-resistant, so a refresh or a second click never resets the clock or stacks trials.
Responses:
| Status | When |
|---|---|
200 | trial issued (or the existing one returned) |
400 | missing/invalid email/product, or the slug exists in more than one tenant (pass tenant) |
403 | trials are not enabled for that product (trialDays <= 0) |
404 | unknown product |
Wire a “Start free trial” button
Because /v1/trial is CORS-open, a static page can call it with fetch:
<button id="trial">Start free trial</button>
<script>
document.getElementById('trial').onclick = async () => {
const res = await fetch('https://keyright.delta1labs.com/v1/trial', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ product: 'nebula', email: userEmail, company: userCompany }),
});
const data = await res.json();
// The key is emailed, not returned — tell the customer to check their inbox.
if (res.ok) show(`Check ${userEmail} for your trial key — it expires ${data.expiresUtc}.`);
else show(data.error);
};
</script>
In your code
The customer receives the trial key by email, then enters it into your app — where it activates exactly like a paid key, with no separate trial code path. You pass it to activate() like any paid key. With Nebula, for example:
nebula register --key LIC-XXXXXXXXXXXXXXXXXXXX
Because a trial key activates through the exact same call as a paid key, there’s no separate trial code path to write — the activate snippet for .NET, Node.js, Python, Java and raw HTTP is the all-language block in Activate, deactivate & move seats; pass the trial key to it unchanged. The CLI form above performs an online activation (POST /v1/activate), binds the machine, and returns a signed lease with trial:true and the trial’s expiry. Your app reads it off LicenseInfo: IsTrial == true, ExpiryUtc is the trial end, and DaysRemaining counts down. Show the evaluation banner (e.g. “Enterprise Trial — 27 days left”) and gate whatever you want behind info.IsTrial. Thanks to the cached lease the app keeps working offline until the trial expires; at expiry the lease stops validating and the app fails closed to the free/unlicensed state (see .NET SDK integration).
Trials are duration-from-first-activation aware and clock-tamper aware — a customer can’t win back time by turning the clock back. Trial → paid is seamless: when the customer buys and activates a non-trial key, it supersedes the leftover trial with no reinstall. The full activation shape for trial, term and perpetual keys is in Activate, deactivate & move seats.
Offline trials for air-gapped evaluators
Self-service trials use the online path, but that is not the only way to trial. For an air-gapped or enterprise evaluator you can also hand out a signed, time-limited offline trial license file — no network required at all. With Nebula’s CLI:
nebula license --sign --trial --tier enterprise --expiry 2026-10-23 \
--licensee "Acme Corp" --private-key ./keys/nebula.private.json --out acme-trial.json
The file carries the trial flag and an expiryUtc; the SDK validates it against the embedded public key and treats it as a trial until it expires. This is the offline analogue of the self-service flow above — same trial semantics, delivered as a file instead of an activation. See Run offline / air-gapped.
Cloud vs Self-hosted
Only two things change between Keyright Cloud and self-hosted: the $BASE URL (Cloud is https://keyright.delta1labs.com) and where $TOKEN comes from. One point is genuinely mode-specific:
- Self-service trials need SMTP. Keyright delivers the trial key by email, so the instance must have SMTP configured (
KEYRIGHT_SMTP_*) — without it the trial is minted but the customer never receives the key. On Keyright Cloud email delivery is already wired; on self-hosted you configure it yourself. - The offline trial file path needs no SMTP (and no network) at all.
Verify
- Enable trials on the product (
days > 0), thenPOST /v1/trialwith a test email — expect200with"emailed": trueand anexpiresUtc. - Check the inbox for the trial key, and confirm the dashboard Licenses view now shows a trial license for that email.
- Activate the key in your app and confirm it shows the evaluation banner with
IsTrial == trueand a counting-downDaysRemaining. - Re-request the trial with the same email — expect the same key back and
"created": false(no new license, no reset clock).
See also
- Getting started — issue and verify your first (paid) license.
- Activate, deactivate & move seats — how trial, term and perpetual keys activate and surface on
LicenseInfo. - Run offline / air-gapped — signed offline license and trial files.
- HTTP API reference — the full endpoint map, including
/v1/trial.