License Types & Control

License Types & Control

License Types & Control

Every licensing model in one platform. Perpetual, subscription, trial, metered, floating, node-locked, bundled — all controlled by the same license key format. Mix and match per product without changing your integration.

Perpetual & Subscription
Set ExpiresAt to null for lifetime licenses or a future date for subscriptions. Renewal extends the date — the key stays the same.
Trial Licenses
Issue trial keys instantly, no card required. Automatically expire on a set date. Convert to paid by extending the expiry — same key, no re-integration.
Floating / Concurrent Licenses
Seat-based licensing with real-time checkout/checkin. Enforce maximum concurrent users across any number of machines.
Node-locked / HWID
Bind a license to a specific machine fingerprint. The activation API tracks distinct device IDs up to the key's seat limit; the same ID reuses its seat.
License Bundles
One key that unlocks multiple products simultaneously. Great for suites — one purchase, one key, access to everything in the bundle.
Feature Flags per License
Store a JSON feature map on each license. Your app reads which modules are enabled at validation time — no new keys required for upsells.
Version Management
Lock a license to a maximum software version. The API checks configured version bounds when your request supplies a version. Maintenance dates are separate metadata.
Offline Activation
ECDSA P-256 signed challenge/response for air-gapped machines. No internet required after initial activation file generation.
Test / Sandbox Mode
Sandbox keys behave identically to production keys but are excluded from billing counters and analytics. Safe for CI/CD pipelines.
License Templates
Save and reuse license configurations — expiry, max activations, feature flags, and metadata. One click to apply a template when creating a new license.
Grace Periods
Offline grace period (validate without network for N days) and post-expiry grace period (continue working N days after expiry). Configurable per license.
Maintenance & Annual Support
Track maintenance contract end dates separately from license expiry. Returned to your app for its own update/support policy; the date does not itself reject validation.

Licensing Features

Advanced License Management

Advanced filtering

Click the Filters button on the Licenses page to open the advanced filter panel. You can filter your license list by multiple criteria simultaneously:

FilterWhat it does
StatusShow only Active, Revoked, or Expired licenses
Expires from / toFind licenses expiring within a date range (e.g. "next 7 days")
Tag containsFilter by tag substring — type "beta" to find all beta licenses
Min activations %Slider (0–100%) — e.g. set to 80% to find nearly-saturated keys

Active filters show an "ON" badge on the Filters button. Click "Clear" to reset all filters at once.

Bulk update

Select multiple licenses using the checkboxes, then click "Edit Selected". The bulk edit modal lets you update up to 500 licenses in a single operation:

FieldBehaviour
Max ActivationsSet a specific number (must be ≥ current activations on each key)
Expiry DateSet a new expiry, or tick "Clear expiry" to make keys perpetual
Add TagsTags are merged with existing tags — existing tags are not removed
Add NotesText is appended to existing notes (one new line per update)
Fields you leave blank are not modified — only fill in what you want to change.

SDK integration snippets

Click the Integrate button in the top-right of the Licenses page to open the SDK snippet generator. It shows copy-ready integration code in curl, Python, Node.js, C#, and PHP — pre-filled with your actual API base URL. Use these to quickly wire up license validation in your application.

Expiry notifications

PermitCore automatically emails Company Admins when licenses are about to expire. Emails are sent at three checkpoints:

  • 30 days before expiry — early warning, plan renewals
  • 7 days before expiry — action required soon
  • 1 day before expiry — urgent reminder

Each license gets at most one email per checkpoint (no daily spam). The email lists every affected key with product name, expiry date, days remaining, and current activation count.

Expiry notification emails require your account to have a working mail provider configured (the deployment mail configuration (managed by the operator)). Emails are sent daily at 08:00 UTC.

Licensing Features

Trial Licenses

Trial licenses let potential customers try your software for a fixed number of days — no manual follow-up required. The license key is valid from creation and automatically expires after the configured trial period.

Creating a trial license

  1. Go to Licenses → New License
  2. Toggle "Trial License" on
  3. Enter the trial period in days (1–365; default 30)
  4. Generate the key and send it to the prospect — expiry is set automatically
Trial licenses are visually marked with a purple Trial badge in the license list and detail view. The expiry date is computed from the moment the license is created, not from when the customer first activates.

Behaviour

SettingTrial licenseRegular license
ExpiryAuto-set to now + TrialDaysOptional, set manually
ValidationStandard — returns isValid: false once expiredStandard
ConversionRevoke + issue new regular key, or extend expiry in Edit Metadata
BadgeIsTrial: true returned in API and portalIsTrial: false

Converting a trial to full license

When a trial customer purchases, you have two options:

  • Extend: Go to the trial license → Edit Metadata → clear or extend Expires At. The customer keeps their existing key.
  • Replace: Revoke the trial key and generate a new non-trial license. Send the customer the new key.

Licensing Features

Test / Sandbox Mode

Sandbox licenses let you integrate and test PermitCore without burning your plan's billing quota. Test licenses work exactly like production licenses — validate, activate, floating seats, feature flags, offline tokens — but their activations are never counted against your MaxTotalActivations billing pool.

No separate sandbox environment is needed. Every tenant has a single environment with both live and test licenses. The filter in the license list defaults to Live only to keep your daily workflow clean.

Creating a test license

  1. Go to Licenses → Create License.
  2. Fill in the form as normal (product, max activations, expiry, features…).
  3. Before encrypting, toggle Test / Sandbox license on.
  4. Proceed through passphrase and generate as usual.

Identifying test licenses

Test licenses are visually distinguished throughout the admin panel:

LocationIndicator
License listYellow Test badge next to the encrypted serial
License list filterDropdown: Live only (default) / Live + Test / Test only
License detailReturned in LicenseDto.isTest = true via API

Billing behavior

ActionLive licenseTest license
ActivationCounts against MaxTotalActivations poolNever counted — always allowed regardless of plan limit
ValidateStandardIdentical — same response shape
Floating check-in/outStandardIdentical
Billing dashboardShows current pool usageExcluded from the displayed count
Cannot switch after creation. The IsTest flag is set at creation time and cannot currently be changed after the license is saved. If you need to promote a test license to production, revoke it and generate a new live license.
Licensing Features

Version Management

Version management lets you enforce software build constraints per license. Set a Min Version and/or Max Version on any license key — customers running builds outside the allowed range are blocked when they call validate or activate.

Use cases: Block builds with known security vulnerabilities (set MinVersion), prevent customers on old support contracts from upgrading past a major version (set MaxVersion), or pin enterprise customers to a specific release while you stabilise the next major version.

Setting version constraints

At creation time — fill in Min Version and/or Max Version in the Licenses → New License form. Both are optional; leave blank for no restriction.

On existing licenses — go to Licenses → View → Edit Metadata. Changes take effect immediately on the next API call from the customer's software.

Version range examples

MinVersionMaxVersionEffect
2.0.0Blocks all builds below 2.0.0
3.9.9Blocks all builds above 3.9.9
2.0.03.9.9Only 2.x and 3.x builds allowed
3.0.03.0.0Pins to exactly version 3.0.0
No restriction — any version passes

Versions are parsed as MAJOR.MINOR.PATCH and compared numerically component-by-component (not as a lexicographic string compare), so 2.10.0 > 2.9.0 orders correctly. Any pre-release/build suffix (-beta1, +build5) is stripped before comparison.

How to send the version from your app

Pass the current software version in the JSON request body on both validate and activate:

curl — validate with version
curl -X POST https://api.permitcore.dev/api/v1/validate \
  -H "Content-Type: application/json" \
  -d '{"licenseKey":"PERMIT-XXXX-XXXX-XXXX-XXXX","version":"2.3.1"}'
curl — activate with version
curl -X POST https://api.permitcore.dev/api/v1/activate \
  -H "Content-Type: application/json" \
  -d '{"licenseKey":"PERMIT-XXXX-XXXX-XXXX-XXXX","version":"2.3.1","nonce":"<from GET /api/v1/nonce>"}'
JavaScript
const res = await fetch('/api/v1/validate', {
  method: 'POST', headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ licenseKey: key, version: '2.3.1' }),
});
const data = await res.json();

if (!data.isValid && data.minVersion) {
  showError(`Please upgrade to v${data.minVersion} or newer.`);
}

Blocked response

JSON — version blocked
{
  "isValid":    false,
  "message":    "Version 1.9.0 is below the minimum required version 2.0.0"
}

Note: the version-blocked response doesn't populate minVersion/maxVersion itself (they come back null) — parse the required version out of message, or call validate() without a version first to read the license's bounds directly.

If the customer sends no version at all, version constraints are skipped — the call proceeds as if there is no restriction. This ensures backward compatibility with apps built before you added version checks.

Version tracking in activations

The version string is stored on every Activation record. This lets you see which software builds your customers are running in the activation history and analytics — useful for planning deprecation timelines or identifying customers who need to upgrade.


Licensing Features

Feature Flags

Feature flags let you control which parts of your software are unlocked for a given license — without issuing separate keys per edition. Instead of "License A = Basic" and "License B = Pro", you attach a list of feature strings to each license and check them at runtime.

Example: Your app has three tiers — Basic, Pro, Enterprise. Instead of three separate products, you use one product and set features to ["export"], ["export","api"], or ["export","api","sso","white-label"] per license. Your code checks the list on every call.
Feature flags in Edit Metadata modal

Feature flags configured in the Edit Metadata modal — comma-separated strings returned in every validate/activate response.

Setting feature flags

  1. Open Licenses → View → Edit Metadata
  2. Enter comma-separated feature names in the Feature Flags field (e.g. export, api, sso)
  3. Save — changes take effect immediately on the next API call

Use consistent lowercase feature names. Raw string comparisons are case-sensitive; some SDK helper methods compare case-insensitively. Grant access only after a valid license decision.

Reading feature flags in your app

JavaScript
const res = await fetch('/api/v1/validate', {
  method: 'POST', headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ licenseKey }),
});
const data = await res.json();

if (!data.isValid) { /* reject */ }

const features = new Set(data.features ?? []);

if (features.has('export')) { enableExport(); }
if (features.has('api'))    { enableApiAccess(); }
if (features.has('sso'))    { enableSso(); }
Python
data = requests.post('/api/v1/validate', json={'licenseKey': license_key}).json()

features = set(data.get('features') or [])

if 'export' in features:
    enable_export()
if 'sso' in features:
    enable_sso()
C#
var httpResp = await http.PostAsJsonAsync("/api/v1/validate", new { licenseKey = key });
var resp = await httpResp.Content.ReadFromJsonAsync<LicenseResult>();
var features = resp?.Features?.ToHashSet() ?? [];

if (features.Contains("export")) EnableExport();
if (features.Contains("api"))    EnableApiAccess();

Common patterns

PatternFeature stringsNotes
Tiered editionsbasic, pro, enterpriseCheck for the highest tier present
Module unlocksexport, api, sso, reportsEach feature is independent
Seat count in featuresseats:10, seats:50Parse the value after the colon
Expiring beta accessbetaCombine with ExpiresAt for time-limited beta


Licensing Features

Offline Grace Period

The offline grace period lets your software continue working even when it cannot reach the PermitCore server — for example, when the customer has no internet, or your server is temporarily down. Set a number of days, and your SDK should cache the last successful validation response and treat it as valid for that duration.

Setting the grace period

  1. Go to Licenses → View → Edit Metadata
  2. Enter a number in the Offline Grace Days field (e.g. 7)
  3. Save — the value is returned in every validate/activate response as offlineGraceDays

Use signed offline grace

When offline grace is enabled, successful responses can include an ECDSA-signed offlineCacheToken with the pc_grace_v1 prefix. Official SDKs cache and verify this token when the service cannot be reached. Configure the SDK and its required cryptography dependencies; do not cache a bare JSON success and trust it later.

A custom implementation must validate token structure, signature, trusted public key, expiry, and matching license/device binding where applicable. Protect public-key distribution as part of your app's trust model. A public key loaded from the same untrusted cache as a token is not independently trustworthy. Do not use offline grace to override an explicit online rejection.

Use admin-issued offline activation for machines that cannot obtain an initial online validation.

Recommendations

ScenarioSuggested grace period
Desktop app (typical internet user)7 days — covers a holiday or travel period
Server-side / always-online software0 or 1 day — connectivity expected
Air-gapped / industrial softwareUse offline activation tokens instead
Mobile app3–7 days
Security note: The cached token is cryptographically signed — forging a fake "valid" entry from nothing now requires PermitCore's private signing key, not just local file-write access. What this still doesn't and can't prevent: a user manipulating their system clock to keep an already-issued token looking unexpired, or copying a genuinely-issued cache file to a second machine (unless your app also checks the payload's device binding). Offline grace is availability protection for legitimate customers going offline, not a DRM/anti-tamper mechanism. For high-security licensing, use shorter grace periods (1–3 days) and consider offline activation tokens (individually revocable, admin-issued) for genuinely air-gapped environments.

Licensing Features

Named User Licensing

By default, PermitCore tracks how many activations a license has consumed and from where (IP address, country, device ID). Named user licensing goes one step further: it records who activated the license — the end user's email address.

This is especially useful for B2B software sold to companies where IT admins need to audit which employees are using which licenses. Instead of seeing "5 activations on 5 devices", you see "activated by [email protected] (2 devices), [email protected] (1 device), [email protected] (2 devices)".

Raw HTTP activation also requires a fresh nonce from GET /api/v1/nonce. The snippets here focus on device/user fields; use the complete activation flow when sending a request.

How to use it

Pass the optional userEmail field when calling POST /api/v1/activate:

JSON — activate with named user
{
  "licenseKey":  "PERMIT-A3K7-MN2P-QR9X-TZ5W",
  "deviceId":    "a3f8d2b1c9e4...",
  "deviceName":  "Alice-ThinkPad",
  "userEmail":   "[email protected]"
}
JavaScript / Node.js
const response = await fetch('https://api.permitcore.dev/api/v1/activate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    licenseKey: key,
    deviceId:   hwid,
    userEmail:  currentUser.email  // pass the logged-in user's email
  })
});

Viewing named user data in the admin panel

Go to Licenses → View → Activation History. The table includes a User Email column that shows the email address recorded at activation time.

Key considerations

  • Optional field — if userEmail is omitted, the activation is recorded without a user email. Licenses without it still work normally.
  • Not enforced — PermitCore does not restrict one email to one seat; it records the field for reporting only. Enforce seat limits via MaxActivations.
  • Max 256 characters — the field is truncated server-side if longer.
  • Immutable after activation — the email is stamped at activation time and not updated on re-activation.
Privacy: User emails are stored only within your PermitCore instance (your database). Use the minimum personal data needed for your workflow and review the privacy policy and subprocessors. Follow your own GDPR obligations when collecting end-user emails.

Licensing Features

License Templates

If you create many licenses with the same configuration — same max activations, same expiry, same feature flags — License Templates let you save that configuration once and apply it in one click when creating new licenses.

Creating a template

  1. Go to Templates in the left sidebar (or navigate to /templates)
  2. Click New Template
  3. Fill in the fields you want to pre-populate:
    • Name — e.g. "Pro Annual", "Starter Trial", "Enterprise Seat"
    • Max Activations — default 1
    • Expires After (days) — leave blank for never-expiring licenses
    • Version Range — MinVersion / MaxVersion to lock the template to specific software versions
    • Activation Period (days) — sliding window from first activation
    • Trial — check if this is a trial template and enter the trial duration
    • Notes — internal description for your team
  4. Click Save Template

Template API

MethodEndpointDescription
GET/api/templatesList all templates for the tenant
GET/api/templates/{id}Get a single template
POST/api/templatesCreate a new template
PUT/api/templates/{id}Update a template
DELETE/api/templates/{id}Delete a template

All template endpoints require a CompanyAdmin JWT token (Authorization: Bearer <token>).

POST /api/templates — request body
{
  "name":                 "Pro Annual",
  "maxActivations":       5,
  "expiresAfterDays":     365,
  "isTrial":             false,
  "trialDays":           0,
  "minVersion":          "2.0.0",
  "maxVersion":          "3.9.9",
  "activationPeriodDays": null,
  "featuresJson":        "[\"export\",\"api\",\"pro\"]",
  "notes":               "Pro plan annual — 5 seats, expires 1 year from issue"
}
Save time at scale: If you issue 500 licenses a month, using templates means you never have to remember which feature flags go with which plan tier — just pick the template and the correct config is applied automatically.

Licensing Features

License Transfer

When a customer needs to move their license to a new device — because their hardware was stolen, replaced, or they changed jobs — License Transfer lets you instantly reset all activations so the license can be used on new machines.

What transfer does

  • Deletes all existing activation records for the license
  • Resets CurrentActivations to 0
  • Leaves the license key itself unchanged — the customer reuses the same key
  • Logs the action in the Activity Log with the number of activations cleared

How to transfer (admin panel)

  1. Go to Licenses → View for the license
  2. In the Activation History header, click Transfer (blue button)
  3. Confirm in the dialog — all activations are cleared immediately
  4. Give the customer their existing key — they can activate on new devices

Transfer API

POST /api/licenses/{id}/transfer
curl -X POST https://api.permitcore.dev/api/licenses/{id}/transfer \
  -H "Authorization: Bearer <admin-token>"

No request body required. Response:

JSON
{
  "message": "License transferred: 3 activation(s) cleared."
}
Irreversible: Transfer permanently deletes the activation records. If you need to keep a history of which devices were ever activated, use the Activity Log — the transfer action is logged there with the count of cleared activations.

Transfer vs. Reset Activations

TransferReset Activations
PurposeCustomer moves to new hardwareAdmin maintenance / troubleshooting
EffectClears activations, keeps keyClears activations, keeps key
Audit log entryYes — "TransferLicense"Yes — "ResetActivations"
Semantic differenceCustomer-facing workflowAdmin-facing workflow


Licensing Features

Node-locked / HWID Licensing

Node-locked licensing ties a license key to a specific device (hardware fingerprint). This is useful for single-machine perpetual licenses where you want to prevent one key from being shared across multiple computers.

How it works

Enable Node Locked when creating a license (or update it after creation). Then in your application:

  1. Generate a device fingerprint — a stable SHA-256 hex string identifying this install. Prefer your SDK's built-in helper (see below) over hashing raw hardware identifiers.
  2. Pass it as deviceId when calling POST /api/v1/activate — or just omit it if you're using an SDK, and it's collected automatically
  3. Optionally pass a human-readable deviceName (e.g. "John-MacBook-Pro")
Node-locked activation
{
  "licenseKey": "PERMIT-A3K7-MN2P-QR9X-TZ5W",
  "deviceId":    "a3f8d2b1c9e4...",
  "deviceName":  "John-MacBook-Pro-M3"
}

Raw HTTP activation also requires a fresh nonce from GET /api/v1/nonce. The snippets here focus on device/user fields; use the complete activation flow when sending a request.

Re-installation is free

When a license is node-locked, re-activating from the same device ID does not consume an additional activation slot. The server recognises the device and counts it as a re-activation rather than a new registration. This means customers can reinstall their OS without using up activations — as long as the hardware fingerprint stays the same.

Choosing a device fingerprint

PermitCore is fingerprint-agnostic — it accepts any stable SHA-256 hex string as deviceId and never generates or interprets it. Use your SDK's built-in helper first — the .NET, Python, Node.js, and PHP SDKs ship a GetHardwareId() / get_hardware_id() helper that's called automatically whenever deviceId is omitted from activate()/ checkout(). It deliberately avoids hashing raw CPU serial, disk serial, or MAC address — those are unstable (cloned on VM snapshots, changed on hardware upgrades, unavailable in containers) and needlessly sensitive to collect. Instead it combines a few low-volatility attributes (hostname, OS, architecture) with a random seed persisted to a local file on first run.

SDKHelperPersisted seed location
.NETPermitCoreClient.GetHardwareId()%APPDATA%\.permitcore_seed
PythonPermitCoreClient.get_hardware_id()~/.permitcore_seed
Node.jsPermitCoreClient.getHardwareId()~/.permitcore_seed
PHPPermitCoreClient::getHardwareId()system temp dir (weaker isolation — avoid relying on it on shared hosting)
Java / C++none yetpass your own deviceId
Why not hash real hardware identifiers? A fingerprint only needs to be stable for one install, not cryptographically tied to physical hardware — this isn't a DRM/anti-tamper mechanism, it's a seat-counting key. Raw CPU/disk/MAC hashing breaks on the cases that matter most in practice (VM re-provisioning, cloud instances, hardware swaps), and since re-activating from a changed fingerprint consumes a new seat on a node-locked license, an unstable ID directly costs your customer activations. If you're on Java, C++, or a custom integration without a built-in helper, prefer an app-generated installation ID persisted to your own config file over hashing hardware attributes.

Persist the installation identity

Use your SDK helper where available. For custom clients, generate an installation identifier once, persist it in the application's protected configuration, and send the same device ID on future activations. Reinstalling or deleting its seed may change that ID. Do not derive a supposedly stable identity from whichever network adapter happens to be returned first.

Node-locked vs. count-based — choosing the right mode

ScenarioRecommended mode
Desktop app, single-machine perpetual licenseNode-locked (Max Activations = 1–2)
Desktop app, allow use on 2–3 personal devicesCount-based (Max Activations = 2–3) or Node-locked (Max Activations = 2–3)
SaaS, web app, or mobile appCount-based — device ID is not meaningful
CI/CD or server software (multiple machines)Count-based with a higher activation limit
Floating / concurrent licenseUse the Floating License type — HWID not applicable

Managing device activations in the admin panel

  1. Go to Licenses and click View on a node-locked license
  2. Scroll to the Active Devices table — each row shows Device ID, Device Name (if provided), and activation timestamp
  3. Click the Deactivate button next to any row to remove that machine's activation
  4. The activation counter decrements immediately, freeing up a slot for the customer to use on a new machine
Deactivating a device from the admin panel does not automatically invalidate tokens or local licenses on that machine. If you need the software to stop working immediately, also revoke the license key entirely, or rely on the online heartbeat in your SDK.

Deactivation API (programmatic)

Your application can let customers self-deactivate through the anonymous customer portal API (e.g. a "Deactivate this machine" button in Settings). First look up the license to get the activationId of the device to remove, then deactivate it by that ID:

POST /api/v1/portal/{tenantSlug}/lookup — find the activationId
// Request body
{
  "licenseKey": "PERMIT-A3K7-MN2P-QR9X-TZ5W"
}

// Response 200 OK — activations[].id is what you need below
{
  "productName": "Task Manager Pro",
  "activations": [
    { "id": "3b4c9f2a-1e7d-4a8f-9c2b-...", "deviceName": "John-MacBook-Pro-M3", "activatedAt": "2026-06-01T00:00:00Z" }
  ]
}
POST /api/v1/portal/{tenantSlug}/deactivate — remove that activation
// Request body
{
  "licenseKey":   "PERMIT-A3K7-MN2P-QR9X-TZ5W",
  "activationId": "3b4c9f2a-1e7d-4a8f-9c2b-..."
}

// Response: 204 No Content on success (empty body)

Both endpoints are unauthenticated (identified by the license key itself) and always take the key/ID in the POST body — never the URL — to keep them out of access logs. After deactivation the customer can activate the key on a new machine; the activation counter is decremented server-side.

If you've enabled the "Key + email code" or "Account required" portal security level (see Security — three levels), this exact key-only flow stops working at that level and your integration needs to change: at "Key + email code", call POST /api/v1/portal/{tenantSlug}/deactivate/request-otp first and pass the returned otpToken plus the code the customer enters as otpCode on the deactivate call above; at "Account required" this endpoint returns 403 outright and you'll need the authenticated /account/licenses/{licenseId}/deactivate endpoint instead. Most vendors stay on the default and never need this.

Licensing Features

Post-Expiry Grace Period

A post-expiry grace period lets a license remain valid for a configurable number of days after its ExpiresAt date. This is useful for customers who are in the process of renewing — it prevents a hard cutoff at the exact expiry moment.

Different from Offline Grace Days. Offline Grace Days covers the case where the customer has no internet and can't reach your server. Post-Expiry Grace Days covers the case where the server is reachable but the license has just passed its expiry date.

How it works

  1. Set PostExpiryGraceDays on a license at creation time (or via Edit License).
  2. After ExpiresAt, POST /api/v1/validate and POST /api/v1/activate still return isValid: true.
  3. The response includes "isInGracePeriod": true so your app can show a renewal reminder.
  4. Once the grace period is exhausted, the key behaves as expired (isValid: false).

Validate response during grace period

POST /api/v1/validate
{ "licenseKey": "PERMIT-XXXX-XXXX-XXXX-XXXX" }

→ {
  "isValid": true,
  "isInGracePeriod": true,
  "expiresAt": "2026-05-01T00:00:00Z",
  "message": "License is valid"
}

Setting grace days

Enter a value (e.g. 7) in the Post-Expiry Grace Days field when creating or editing a license. Leave blank for no grace period (license expires hard at ExpiresAt).


Licensing Features

Maintenance / Annual Support Period

Maintenance expiry covers the classic desktop ISV model: a customer buys a perpetual license (activates forever) but only receives software updates and support while their annual support contract is active.

How it works

Set MaintenanceExpiresAt on a license. The validation response always includes this date. Your SDK or application logic decides what to do with it — typically blocking upgrades to versions released after the maintenance expiry date.

Validate response

POST /api/v1/validate
{ "licenseKey": "PERMIT-XXXX-XXXX-XXXX-XXXX" }

→ {
  "isValid": true,
  "maintenanceExpiresAt": "2027-01-15T00:00:00Z",
  "message": "License is valid"
}

Recommended SDK pattern

var result = await client.ValidateAsync(key);
if (result.IsValid && result.MaintenanceExpiresAt.HasValue
    && result.MaintenanceExpiresAt.Value < DateTime.UtcNow)
{
    // Block upgrade — maintenance expired. Allow running current version.
    ShowMessage("Your maintenance plan has expired. Upgrade to re-enable updates.");
}
PermitCore does not enforce the maintenance date itself — it only surfaces it in the API response. Your application is responsible for deciding which software versions require an active maintenance contract. This gives you full flexibility over your release policy.

Setting maintenance expiry

Pick a date in the Maintenance Expires At field in the Create License form. For perpetual licenses, leave ExpiresAt blank and only set MaintenanceExpiresAt.



Licensing Features

License Bundles Professional+

A bundle groups multiple products under a single license key. When a customer activates their bundle key, the API response includes every product they're entitled to — without issuing separate keys for each product.

Use case: You sell "Acme Suite" — a bundle of Acme Video + Acme Audio + Acme Export. Instead of sending 3 separate keys, the customer gets one bundle key. Your app checks the bundleProducts array to know which modules to unlock.

Creating a bundle

  1. Go to Bundles in the sidebar.
  2. Click New Bundle, give it a name, and select at least 2 products.
  3. Save. The bundle now appears as a selectable product type when creating license keys.

Validate / Activate response

JSON
{
  "isValid": true,
  "productName": "Acme Suite",
  "bundleProducts": [
    { "productId": "...", "productName": "Acme Video" },
    { "productId": "...", "productName": "Acme Audio" },
    { "productId": "...", "productName": "Acme Export" }
  ]
}

bundleProducts is a list of objects (productId + productName), not plain strings. Prefer stable productId values for entitlement checks; names can change.

SDK pattern

C#
var result = await client.ValidateAsync(licenseKey);
if (result.IsValid)
{
    var products = result.BundleProducts?.Select(p => p.ProductName) ?? Enumerable.Empty<string>();
    if (products.Contains("Acme Video")) EnableVideoModule();
    if (products.Contains("Acme Audio")) EnableAudioModule();
}

Constraints

  • A bundle requires at least 2 products.
  • Deleting a bundle is blocked if active license keys exist for it.
  • Bundle keys can be sold via the License Store — customers see the bundle as a single product with all included items listed.