KeyPort LogoKeyPort
Back to all guides
Tutorial• April 2026

How to Handle License Expiry in Your Desktop App

Step-by-step implementation of expiry handling: grace periods, user messaging, and fallback behavior for expired desktop app licenses.

SC
Sarah ChenAuthor • 5 min read

License expiry is easy to implement technically but easy to get wrong from a UX perspective. Show the wrong error at the wrong time and you'll get angry support emails from users who just had a payment processing delay.

Here's the complete implementation pattern.

The Three Expiry States

  1. Active — valid, full access
  2. Near-expiry — 14 days left, show a non-blocking reminder
  3. Expired — past expiry, show a blocking screen with a renewal CTA
Active Full access No action needed ! Near-Expiry ≤14 days left Show banner reminder Expired Past expiry date Block + renewal CTA
const result = await validateLicense(key);

if (result.valid) {
  const expiresAt = result.expires ? new Date(result.expires) : null;
  const daysLeft  = expiresAt
    ? Math.ceil((expiresAt.getTime() - Date.now()) / 86_400_000)
    : null;

  if (daysLeft !== null && daysLeft <= 14) {
    showRenewalReminder(daysLeft); // non-blocking banner
  }

  launchApp();
} else if (result.status === 'expired') {
  showExpiredScreen();
} else {
  showBlockedScreen(result.status);
}

Near-Expiry Reminder Copy

Your license expires in 7 days. Renew now to keep uninterrupted access. Renew →

Show this as a dismissible banner, not a modal. Don't block the user — they're doing real work. The message is factual and calm, not threatening.

Expired Screen

When truly expired, block access with a clear screen that:

  • States that the license has expired (not "invalid key" or cryptic errors)
  • Shows the exact expiry date
  • Provides a direct renewal link
  • Optionally: a "Retry" button for users whose payment just went through

Offline Grace Period

Never hard-block users who can't reach the internet. Always implement a grace period:

const GRACE_DAYS = 7;
const CACHE_KEY = 'kp_license_cache';

async function validateWithGrace(key: string): Promise<ValidationResult> {
  try {
    const result = await callValidateAPI(key);

    if (result.valid) {
      localStorage.setItem(CACHE_KEY, JSON.stringify({
        ...result, cachedAt: Date.now()
      }));
    }
    return result;

  } catch {
    const cached = JSON.parse(localStorage.getItem(CACHE_KEY) || 'null');
    if (!cached) return { valid: false, status: 'network_error' };

    const ageDays = (Date.now() - cached.cachedAt) / 86_400_000;
    if (ageDays <= GRACE_DAYS) return { ...cached, offline: true };
    
    return { valid: false, status: 'offline_grace_exceeded' };
  }
}

Status → UI Mapping

StatusShow userAction
activeLicense is validAllow full access
expired"License expired on [date]"Renew link
revoked"This license has been disabled"Contact support
ip_blocked"Access restricted from this location"Contact support
not_found"Invalid license key"Show error

Testing Expiry Flows

In KeyPort, manually set an expiry date to a past date on a test license. Use this to verify your expired and near-expiry UI before shipping. Expiry UX bugs discovered when a real customer's license expires are the worst possible time to find them.

Scale your product with KeyPort

Free tier available for launch and small production workloads. No credit card required.