POST /validate

POST /api/v1/validate

Subscription renewals & past_due

For monthly and annual Store purchases, a paid Stripe subscription invoice extends the existing license to the end of its paid billing period. The customer keeps the same key. expiresAt is the paid-through date, not the grace deadline.

A failed renewal sets paymentStatus to past_due. Validation and activation continue through the configured payment grace window: 14 days by default, measured from the paid-through date. Vendors can set 0–365 days in Store → Settings; a license's explicit PostExpiryGraceDays takes precedence, including zero. unpaid keeps the same deadline. Once it is reached, access expires. A successful payment restores active and advances the paid-through date.

During payment grace, the response includes isInGracePeriod: true and paymentStatus: "past_due" or "unpaid". Show a card-update notice while isValid remains true. Non-subscription licenses return no payment state. Offline cache tokens and newly generated offline activation tokens for Store subscriptions are capped at paid-through plus grace. Reconnect to receive a refreshed signed cache token after payment; already-issued offline tokens retain their signed deadline and cannot learn about a cancellation while disconnected.

Scheduling cancellation at period end preserves access through that paid period. When Stripe ends the subscription, customer.subscription.deleted revokes the license. Duplicate invoices and webhook deliveries have no additional effect; older payments never shorten the paid period or reactivate a revoked key.

Register invoice.paid, invoice.payment_failed, and customer.subscription.updated alongside checkout.session.completed, payment_intent.payment_failed, checkout.session.expired, customer.subscription.deleted, and charge.refunded on the vendor's Store webhook endpoint. This is the customer's Store subscription; the vendor's own PermitCore billing is managed separately.

Checks whether a license key is valid. Returns license info but does not increment any counter. Use this on every app launch to confirm the key is still active. The key is sent in the POST body (never the URL) to keep it out of access logs and browser history.

Request fields

licenseKey is required. Optional version contains the running app version; send it consistently when using version restrictions, since omitting it skips the version check.

Validation checks license entitlement, not proof that the current machine owns a registered activation seat.

Check the product, or pass expectedProductId: a valid, active key from your tenant validates successfully even if it was issued for a completely different product — /validate finds a key purely by the key itself. Either compare the response's productId against your own product's ID yourself, or pass an optional expectedProductId (GUID) in the request and let the server reject a mismatch for you with errorCode: "WrongProduct". Find your product's ID in the Admin panel under Products, or on a license's own detail page.

Examples

cURL
curl -X POST https://api.permitcore.dev/api/v1/validate \
  -H "Content-Type: application/json" \
  -d '{"licenseKey":"PERMIT-A3K7-MN2P-QR9X-TZ5W","expectedProductId":"7f3e9a2b-1c4d-4e8f-9a6b-3d2c1e0f5a8b"}'
JavaScript
const res = await fetch('https://api.permitcore.dev/api/v1/validate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    licenseKey: 'PERMIT-A3K7-MN2P-QR9X-TZ5W',
    // Optional — reject a key issued for a different product instead of silently accepting it
    expectedProductId: '7f3e9a2b-1c4d-4e8f-9a6b-3d2c1e0f5a8b',
  }),
});
const data = await res.json();

if (data.isValid) {
  console.log('License OK for:', data.productName, data.productId);
  // data.remainingActivations tells you how many more activations are allowed
} else if (data.errorCode === 'WrongProduct') {
  console.error('This key is for a different product:', data.productId);
} else {
  console.error('Invalid license:', data.message);
  // Show error to user
}
Python
import requests

resp = requests.post(
    'https://api.permitcore.dev/api/v1/validate',
    json={
        'licenseKey': 'PERMIT-A3K7-MN2P-QR9X-TZ5W',
        # Optional — reject a key issued for a different product instead of silently accepting it
        'expectedProductId': '7f3e9a2b-1c4d-4e8f-9a6b-3d2c1e0f5a8b',
    }
)
data = resp.json()

if data['isValid']:
    print(f"Valid! Product: {data['productName']} ({data['productId']}), "
          f"Remaining activations: {data['remainingActivations']}")
elif data.get('errorCode') == 'WrongProduct':
    print(f"This key is for a different product: {data['productId']}")
else:
    print(f"Invalid: {data.get('message', 'unknown error')}")
C#
using var http = new HttpClient();
var resp = await http.PostAsJsonAsync(
    "https://api.permitcore.dev/api/v1/validate",
    new {
        licenseKey = "PERMIT-A3K7-MN2P-QR9X-TZ5W",
        // Optional — reject a key issued for a different product instead of silently accepting it
        expectedProductId = "7f3e9a2b-1c4d-4e8f-9a6b-3d2c1e0f5a8b",
    }
);
var result = await resp.Content.ReadFromJsonAsync<LicenseResult>();

if (result?.IsValid == true)
    Console.WriteLine($"Valid! Product: {result.ProductName} ({result.ProductId})");
else if (result?.ErrorCode == "WrongProduct")
    Console.WriteLine($"This key is for a different product: {result.ProductId}");
else
    Console.WriteLine($"Invalid: {result?.Message}");

record LicenseResult(
    bool IsValid,
    string? ProductName,
    Guid? ProductId,
    int? RemainingActivations,
    DateTime? ExpiresAt,
    string? Message,
    string? ErrorCode,
    Dictionary<string, string>? CustomFields
);

Success response (200 OK)

JSON
{
  "isValid": true,
  "productName": "Acme Video Editor Pro",
  "productId": "7f3e9a2b-1c4d-4e8f-9a6b-3d2c1e0f5a8b",
  "remainingActivations": 3,
  "expiresAt": null,
  "customFields": { "edition": "Pro", "seats": "5" },
  "features": ["export", "api", "sso"],
  "isTrial": false,
  "trialDaysRemaining": null,
  "nodeLocked": false,
  "offlineGraceDays": 7,
  "vendorWarning": null
}

Invalid / revoked key

JSON
{
  "isValid": false,
  "message": "License key not found",
  "errorCode": "NotFound",
  "productId": null
}

Additional fields include errorCode, minVersion, maxVersion, bundleProducts, isInGracePeriod, maintenanceExpiresAt, and signed offlineCacheToken. These may be null when not applicable. A maintenance date is metadata for your app's update policy, not an automatic license rejection.

Response fields

FieldTypeDescription
isValidbooleantrue if the key exists, is active, not expired, and not revoked
productNamestringThe product this key belongs to
productIdstring (GUID) | nullStable ID of the product this key belongs to. Always present when a license was found (even if you didn't pass expectedProductId) — compare it yourself, or use expectedProductId and let the server do it.
remainingActivationsnumbermaxActivations minus currentActivations — how many more devices can activate
expiresAtstring | nullISO 8601 expiry datetime, or null for perpetual licenses
customFieldsobject | nullArbitrary key-value pairs attached to the license
featuresstring[] | nullFeature flag strings e.g. ["export","api","sso"]. Use these to conditionally unlock functionality in your app without issuing separate keys.
isTrialbooleantrue if this is a time-limited trial license
trialDaysRemainingnumber | nullDays left on the trial period. Only present when isTrial is true. Show this to the customer to prompt them to purchase.
nodeLockedbooleantrue if the license is bound to specific devices (HWID mode)
offlineGraceDaysnumber | nullHow many days your SDK may run without contacting the server. Use the signed offlineCacheToken with a trusted public key; do not trust an editable cached JSON response. null = no grace period.
messagestringReason for invalidity — only present when isValid = false
vendorWarningstring | nullPresent when the vendor's PermitCore subscription is nearing expiry. Surface this to the user. null during normal operation.
Always check vendorWarning in your app: When this field is non-null, show it to the user (e.g. in a settings dialog or tooltip). It tells them the vendor's subscription is expiring and they may lose access soon — prompting them to contact the vendor to renew.