Documentation · SoftActivate Licensing

Activation and validation

The whole integration is a single call, validate_license / ValidateLicenseAsync, which behaves differently depending on what you hand it. This page covers when to call it and what to do with the result.

Day 0: activation

Call validate with no stored license. The SDK registers this device with the service; this first call must be online. The service returns a license, which you retrieve with get_updated_license() / result.UpdatedLicense. Persist it atomically, and consider keeping a backup copy: a lost license file costs your customer a re-activation, which holds a seat until the old one is deactivated.

A few first-activation failures deserve distinct handling in your UI: all seats already in use on a standard license (point the customer at deactivating the old machine), SEAT_LIMIT on a floating license (the error carries a retry hint), PAYMENT_REQUIRED on a lapsed subscription, and plain network failure, where the fix is to retry later; nothing else is wrong.

Every start: local validation

Load the stored license, set it on the validation parameters, and call validate. While the license is current this needs no network and returns VALID in milliseconds; only in the second half of the license's window does the call also renew it online (maximum offline period). Two rules keep the rest of the lifecycle automatic:

  1. Always save the updated license when the SDK issues one. That is how renewals reach disk.
  2. Keep validating even after a failure. Renewal is self-healing: the moment the customer pays, or connectivity returns, the next validation call fixes it.

Renewal is silent

When a license lapses, the next validation call contacts the service once and returns a license reflecting whatever the customer has paid for by then. A subscription renewal needs no code on your part (subscriptions guide).

Choosing the maximum offline period

A license certificate is valid for a window, and that window is the re-verification period. During the second half of the window, each validate call also re-verifies (renews) the license with the service; a window that already ends with the license itself, at the end of a trial or a fixed term, renews once it has ended instead. If the service cannot be reached, the license stays VALID, and the next call tries again. A device that does not reach the service for the whole window gets EXPIRED until it reconnects; the next validate with connectivity renews the license automatically.

On non-floating licenses, this window is the Maximum offline period, set in the console on the license template or the product (products and license terms). Empty means the default of one year, and the minimum is one day. On floating licenses the same field is the Lease duration, with a minimum of 15 minutes (floating licenses).

A longer period lets customers stay offline longer. A shorter one makes revocations take effect sooner: a revocation reaches an offline device when its window ends, at the latest, while an online device sees it at its next start, through the revocation list (revocation). Re-verifying is not an activation, so a short period costs no activations: renewals never count toward Total activations or Activations per device, on any license type.

If you are moving from LimeLM (TurboActivate), nDaysBetweenChecks = 30 plus nGraceDaysOnInetErr = 10 corresponds to a 40-day maximum offline period: the device re-verifies from day 20 and stops validating at day 40 without contact.

The license's end and the end of the window

A license reports two dates (SDK 1.4.0 and later):

  • The expiration date, when the license itself ends: a fixed term's end date, a subscription's paid-through date plus its grace period, or this machine's trial end. get_expiration_date() (unix seconds) in C++, License.ExpirationDate in .NET. A license that never ends returns SWS_LICENSE_NO_EXPIRATION in C++ and License.NoExpiration in .NET, the last second of the year 9999, so a "days left" figure computed from it is never negative.
  • The offline expiration date, the end of the current window: until when the license keeps validating without reaching the service. get_offline_expiration_date() (unix seconds) in C++, License.OfflineExpirationDate in .NET. It is never later than the expiration date.

Warning before the period runs out

When the time left offline is short and the last validation could not reach the service, ask the user to connect to the internet before the license stops validating. The time left is the offline expiration date minus now: get_offline_expiration_date() (unix seconds) in C++, License.OfflineExpirationDate in .NET. A renewal attempt that got no answer at all leaves the status VALID and reports the error code network or timeout, with HTTP status 0 (get_error_code() and get_error_http_status(); .NET result.Error):

C++

#include "softactivate/sws_licensing.h"
#include <ctime>
#include <fstream>
#include <sstream>
#include <string>

auto client = sws::licensing::client::create(endpointUrl);
client->set_metadata("trusted_domain", trustedDomain);
client->set_vendor_id(vendorId);
client->set_license_key(customerLicenseKey);

// the license saved at activation
std::ifstream file("license.dat");
std::stringstream buffer;
buffer << file.rdbuf();
auto stored = std::make_unique<sws::licensing::license>(buffer.str().c_str());

auto params = client->create_license_validation_params();
params->set_license(stored);

auto result = client->validate_license(params);

if (auto renewed = result->get_updated_license())
{
    // re-verified: a new window starts - persist it as usual
    std::ofstream("license.dat") << renewed->serialize();
}
else if (result->get_status() == SWS_LICENSE_STATUS_VALID)
{
    // in the second half of the window, validation also tries to renew;
    // "network" or "timeout" (HTTP status 0) means that attempt got no answer
    std::string code = result->get_error_code();
    bool unreachable = code == "network" || code == "timeout";

    long long seconds_left = stored->get_offline_expiration_date() - std::time(nullptr);

    if (unreachable && seconds_left < 7 * 24 * 60 * 60)
    {
        // ask the user to connect to the internet before the license stops
        // validating, e.g. "Please connect within N days to keep working"
    }
}

C#

using System;
using System.IO;
using SWS.Licensing;

var client = new LicensingClient(endpointUrl)
{
    VendorId = vendorId,
    LicenseKey = customerLicenseKey,
    TrustedDomain = trustedDomain
};

// the license saved at activation
License stored = License.Parse(File.ReadAllText("license.dat"));

LicenseValidationResult result = await client.ValidateLicenseAsync(stored);

if (result.UpdatedLicense != null)
{
    // re-verified: a new window starts - persist it as usual
    File.WriteAllText("license.dat", result.UpdatedLicense.Serialize());
}
else if (result.IsValid)
{
    // in the second half of the window, validation also tries to renew;
    // "network" or "timeout" (HttpStatus 0) means that attempt got no answer
    string? code = result.Error?.Code;
    bool unreachable = code == "network" || code == "timeout";

    TimeSpan remaining = stored.OfflineExpirationDate - DateTimeOffset.UtcNow;

    if (unreachable && remaining < TimeSpan.FromDays(7))
    {
        // ask the user to connect to the internet before the license stops
        // validating, e.g. "Please connect within N days to keep working"
    }
}

VB.NET

Imports System
Imports System.IO
Imports SWS.Licensing

Dim client As New LicensingClient(endpointUrl) With {
    .VendorId = vendorId,
    .LicenseKey = customerLicenseKey,
    .TrustedDomain = trustedDomain
}

' the license saved at activation
Dim stored As License = License.Parse(File.ReadAllText("license.dat"))

Dim result As LicenseValidationResult = Await client.ValidateLicenseAsync(stored)

If result.UpdatedLicense IsNot Nothing Then
    ' re-verified: a new window starts - persist it as usual
    File.WriteAllText("license.dat", result.UpdatedLicense.Serialize())
ElseIf result.IsValid Then
    ' in the second half of the window, validation also tries to renew;
    ' "network" or "timeout" (HttpStatus 0) means that attempt got no answer
    Dim code As String = result.Error?.Code
    Dim unreachable As Boolean = (code = "network" OrElse code = "timeout")

    Dim remaining As TimeSpan = stored.OfflineExpirationDate - DateTimeOffset.UtcNow

    If unreachable AndAlso remaining < TimeSpan.FromDays(7) Then
        ' ask the user to connect to the internet before the license stops
        ' validating, e.g. "Please connect within N days to keep working"
    End If
End If

Clock skew and manipulation

Validation relies on the local clock. Winding the clock back buys a limited amount of time at most, because periodic renewal bounds the damage automatically, so you do not need your own clock heuristics unless your threat model calls for them. Setting the clock forward and then correcting it can produce NOT_YET_VALID; when that happens, tell the customer to fix their clock.

Verify the product

If you sell more than one product, declare which one this application is before validating: client->set_product_id("PRO_EDITION") (.NET client.SetProductId("PRO_EDITION")), using the product's Reference Id from the console's Products page. Two things then hold without further code: a key sold for a different product is refused at activation, before it consumes a seat, and a stored license for a different product fails validation with BAD_PRODUCT. Skip the call and any license of your account validates in any of your apps, which is fine while you sell one product, not once you sell two. The full contract, including reading the license's product and metadata back out, is the product binding guide.

Statuses that deserve their own handling

A few statuses deserve handling distinct from a generic error:

  • EXPIRED: apply your grace policy, and keep validating.
  • PAYMENT_REQUIRED: your dunning moment. Show the subscription state and a link to renewal, not a generic error.
  • BAD_CONTEXT: the license is bound to a different machine. Offer the move flow.
  • BAD_PRODUCT: the key belongs to a different product of yours. Tell the customer which product it matches (it is on their receipt) and where to get the right one.
  • REVOKED: terminal. Direct the customer to support.
  • BAD_SIGNATURE: a damaged license file. Delete it and re-activate.

The full table with recommended actions is the statuses reference.

Threading

Create one licensing client per thread, or serialize access to a shared one. The client object is not thread-safe.