Skip to content

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.

The Acme App product page in Keyright with a Trial card reading '14-day trial · pro' and the note '14 days on the pro edition with 1 seat(s). Anyone can request one with their work email.', beside the product's editions (enterprise, pro, starter) and its per-product signing key.
The product's Trial card sets the three things a trial is made of — length (14 days), tier (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.

FieldRequiredNotes
productyesthe product slug
emailyesthe trial is keyed to this address
companynobecomes the licensee name; defaults to the email
tenantnoonly 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:

StatusWhen
200trial issued (or the existing one returned)
400missing/invalid email/product, or the slug exists in more than one tenant (pass tenant)
403trials are not enabled for that product (trialDays <= 0)
404unknown 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

  1. Enable trials on the product (days > 0), then POST /v1/trial with a test email — expect 200 with "emailed": true and an expiresUtc.
  2. Check the inbox for the trial key, and confirm the dashboard Licenses view now shows a trial license for that email.
  3. Activate the key in your app and confirm it shows the evaluation banner with IsTrial == true and a counting-down DaysRemaining.
  4. Re-request the trial with the same email — expect the same key back and "created": false (no new license, no reset clock).

See also