Offer a free trial
A trial is an ordinary active licence with an end date. Your app checks and activates a trial key like any other, so it needs no code of its own for trials: it only shows how long is left. There are three ways to issue one.
| Where the trial starts | How | What happens at the end |
|---|---|---|
| You give someone a trial | The Console’s issue dialog, with a trial and an end date | The key expires; a purchase issues a new licence and key |
| Your website’s “Try it” form | Your backend calls the Admin API with trial: true |
The key expires; a purchase issues a new licence and key |
| A subscription that starts with a trial | Your Polar or Stripe integration issues it | It converts when the customer pays, with the same key |
Issue a trial from your backend
Section titled “Issue a trial from your backend”Send trial: true with an expires_at, the end of the trial. A trial needs an end date: without
expires_at the request answers 400 validation_failed.
curl https://api.sealcord.com/v1/admin/licences \ -H "authorization: Bearer $SEALCORD_TOKEN" \ -H 'content-type: application/json' \ -d '{ "product_id": "acme-notes", "email": "ada@example.com", "limit_kind": "machines", "limit_count": 1, "trial": true, "expires_at": "2027-01-15T00:00:00Z" }'// Node.js 18 or later, as an ES module (a .mjs file) for the top-level await.const token = process.env.SEALCORD_TOKEN;if (!token) throw new Error('SEALCORD_TOKEN is not set');const response = await fetch('https://api.sealcord.com/v1/admin/licences', { method: 'POST', headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json', }, body: JSON.stringify({ product_id: 'acme-notes', email: 'ada@example.com', limit_kind: 'machines', limit_count: 1, trial: true, expires_at: '2027-01-15T00:00:00Z', }),});const json = await response.json();if (!response.ok) { console.error(`HTTP ${response.status}`, json); process.exit(1);}console.log(json);// Cargo.toml: reqwest = { version = "0.13", features = ["blocking", "json"] }// serde_json = "1"use reqwest::blocking::Client;use serde_json::{json, Value};
fn main() -> Result<(), Box<dyn std::error::Error>> { let token = std::env::var("SEALCORD_TOKEN")?; let response = Client::new() .post("https://api.sealcord.com/v1/admin/licences") .bearer_auth(token) .json(&json!({ "product_id": "acme-notes", "email": "ada@example.com", "limit_kind": "machines", "limit_count": 1, "trial": true, "expires_at": "2027-01-15T00:00:00Z", })) .send()?; let status = response.status(); let body: Value = response.json()?; if !status.is_success() { return Err(format!("HTTP {status}: {body}").into()); } println!("{body:#}"); Ok(())}// Go 1.18 or later: go run main.go
package main
import ( "bytes" "encoding/json" "fmt" "log" "net/http" "os")
func main() { token := os.Getenv("SEALCORD_TOKEN") if token == "" { log.Fatal("SEALCORD_TOKEN is not set") } url := "https://api.sealcord.com/v1/admin/licences" body, err := json.Marshal(map[string]any{ "product_id": "acme-notes", "email": "ada@example.com", "limit_kind": "machines", "limit_count": 1, "trial": true, "expires_at": "2027-01-15T00:00:00Z", }) if err != nil { log.Fatal(err) } req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var answer any if err := json.NewDecoder(res.Body).Decode(&answer); err != nil { log.Fatal(err) } out, _ := json.MarshalIndent(answer, "", " ") if res.StatusCode/100 != 2 { log.Fatalf("HTTP %d: %s", res.StatusCode, out) } fmt.Println(string(out))}// .NET 8 or later: the Program.cs of a console app (dotnet new console).using System.Net.Http.Headers;using System.Net.Http.Json;
var token = Environment.GetEnvironmentVariable("SEALCORD_TOKEN") ?? throw new InvalidOperationException("SEALCORD_TOKEN is not set");using var http = new HttpClient();http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await http.PostAsJsonAsync("https://api.sealcord.com/v1/admin/licences", new{ product_id = "acme-notes", email = "ada@example.com", limit_kind = "machines", limit_count = 1, trial = true, expires_at = "2027-01-15T00:00:00Z",});var json = await response.Content.ReadAsStringAsync();if (!response.IsSuccessStatusCode){ Console.Error.WriteLine($"HTTP {(int)response.StatusCode}: {json}"); Environment.Exit(1);}Console.WriteLine(json);// Java 11 or later: java Main.javaimport java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;
public class Main { public static void main(String[] args) throws Exception { String token = System.getenv("SEALCORD_TOKEN"); if (token == null) { throw new IllegalStateException("SEALCORD_TOKEN is not set"); } String body = "{" + "\"product_id\": \"acme-notes\", " + "\"email\": \"ada@example.com\", " + "\"limit_kind\": \"machines\", " + "\"limit_count\": 1, " + "\"trial\": true, " + "\"expires_at\": \"2027-01-15T00:00:00Z\"" + "}"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.sealcord.com/v1/admin/licences")) .header("authorization", "Bearer " + token) .header("content-type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() / 100 != 2) { System.err.println("HTTP " + response.statusCode() + ": " + response.body()); System.exit(1); } System.out.println(response.body()); }}The key carries the end date, so your app can show the days left with no call, and stops accepting the key offline once it has passed. Limit it to one machine, as above, unless the trial is for a team. Send the key to the person who asked for it, as for a sale (Issue from your backend).
Keep one trial per person on your side: Sealcord issues as many as you ask for.
What the app does
Section titled “What the app does”- During the trial, it runs as on any licence. An
activeverdict on a trial is valid at most a day past the trial’s end (verdicts). - After the trial, activation answers
403 licence_expiredand the stored verdict runs out. Show the end of the trial and where to buy, not an error (what to show). - When the customer buys, a trial you issued by hand or from your backend is not extended: your checkout issues a new licence with a new key, which the customer enters. A trial from a Polar or Stripe subscription converts in place, and the key stays the same.
Trials and your plan
Section titled “Trials and your plan”A trial counts as an active licence until it ends, like any other. On the Free plan, with 30 active licences, a busy trial form can fill the plan: new keys, paid ones too, pause at the limit (plans). Keep trials short, and move to a paid plan before you open a trial form to everyone.
Questions
Section titled “Questions”Can I extend a trial?
Section titled “Can I extend a trial?”Issue a new trial licence with a later end date. A licence’s dates are in its key, so they never change after issue.
How do I see who is on a trial?
Section titled “How do I see who is on a trial?”In the Console, trials show as active licences with a trial end. With the Admin API, a licence on a
trial has trial_ends_at set; it is null for every other licence.
Does a trial need a payment provider?
Section titled “Does a trial need a payment provider?”No. A trial issued by hand or from your backend involves no provider. Only a subscription that starts with a trial comes from one.