"My License Key Doesn't Work": A Support Triage Playbook
“It doesn’t work” is not a bug report. It is a customer telling you they are stuck, and nothing else.
Everything useful is missing from that sentence. You do not know whether they have a key at all, whether it is the right key, whether it reached the app intact, or whether your server rejected it for a reason you could have shown them on screen. So you write back asking questions, they reply a day later, you ask a follow-up, and a five minute problem takes three days of calendar time.
The fix is not a better apology template. The fix is a decision tree that converts a vague complaint into a specific reason code in one reply, plus a small amount of app work so that the app answers the question instead of the customer.
Here is the honest root cause of most of these tickets: your app shows “Invalid license key” for seven different failures. That single string forces the customer to become the diagnostician, and they are bad at it, because they cannot see anything.
The one reply that ends the guessing
Ask for two things, and only two things: the exact error text the app displayed, word for word, and the email address used at checkout.
Those two facts collapse most of the tree. The error text tells you which branch you are on if your app has specific error states, and tells you that your app has no specific error states if it comes back as “invalid license key.” The checkout email tells you whether a license exists at all, whether it went to a typo’d address, and whether they are looking at the right product.
Do not ask for their OS version, their key, and a screenshot in the same message. Every extra question adds a round trip and lowers the reply rate.
This post is about diagnosis. Cutting the number of tickets that reach you in the first place is a different problem, covered in reducing license activation support tickets.
Seven reasons a key fails, in order of frequency
Nearly every activation failure is one of seven things. Work them in this order, because the order roughly matches how often they occur, and the first two are more common than the other five combined.
1. They never received the email. The symptom is a customer who cannot produce a key at all, or who forwards you a receipt instead of a license. They may not even realize a key was supposed to arrive.
The confirming question: what address did you use at checkout? Then check whether a license was issued to it and whether the send actually happened. Three causes sit underneath this one: the email landed in spam or the Promotions tab, the address was mistyped at checkout, or your delivery failed. Check your own send logs before you tell anyone to look in spam. Blaming a spam folder for your bounced send wastes another round trip.
The fix is resending to a corrected address, and, if the send failed, treating it as an outage rather than a ticket. One broken sending domain silently hits every purchase that week.
2. Copy and paste damaged the key. The symptom is a customer who has a key, is certain they typed it correctly, and gets rejected instantly. Instantly matters. A rejection with no network round trip usually means the string failed a format check.
The confirming question: are you pasting from an email, and can you paste the key here exactly as you have it? Then look at what arrives. You will see trailing spaces, non-breaking spaces, a line break in the middle because the mail client wrapped it, curly quotes wrapped around it because someone’s client autoformatted, or an O where a 0 belongs because they retyped it by hand off a phone screen.
The fix is not telling the customer to try again more carefully. The fix is in the next section, and it is your job, not theirs.
3. The device limit is already used up. The symptom is a key the server accepts as valid but refuses to activate, often for a customer who has been happily using your app for a year.
The confirming question: have you replaced or reformatted a computer since you bought this? The usual story is two dead laptops holding two of three seats. The customer is not sharing the key. They are the victim of hardware turnover.
The fix is freeing the seat, and the durable fix is a customer portal where they can remove an old machine themselves at 11pm without you. If this branch is a large share of your tickets, your cap is too low for how your customers actually work. The mechanics of seat binding are in how device activations work, and device activation limits covers where to set the number.
4. Clock skew or an expired offline lease. The symptom is an app that worked yesterday and stopped today, with no purchase, no reinstall, and no change the customer can name. Sometimes it is a machine that was offline for a while. Sometimes it is a machine whose date is set to 2019.
The confirming question: has this machine been offline recently, and is the system date correct? If your app validates a signed lease locally, an expired lease and a wrong clock look identical from the inside, and both present as a sudden loss of access. See how offline license validation works for why the expiry check is the fragile part.
The fix is a network connection and a lease refresh. The design fix is a grace window generous enough that a two week vacation does not lock anyone out.
5. The license was genuinely revoked. The symptom is an activation that fails cleanly, often for someone who has an old chargeback or a refund they may have forgotten about.
The confirming question, asked to yourself and not to them: does a revocation record exist for this license, and what triggered it? Refund, chargeback, or manual action. Check before you reply, because accusing a paying customer of a chargeback they did not file is the worst possible outcome of this branch.
If it was a refund, the answer is simply that the license ended with the refund, and the customer usually already knows. What happens after a refund covers the mechanics, and refund revocation covers the automation.
6. Wrong product or wrong account. The symptom is a key that looks perfectly valid and gets rejected as unknown. This happens when you sell more than one app, when a customer bought the iOS version and is pasting it into the Mac app, or when a company bought under one email and an employee is activating with another.
The confirming question: which product did you purchase, and is the key you are pasting from that purchase’s email? Encoding a product prefix into the key format makes this branch answerable in two seconds by eye, which is one of several reasons key format matters more than people expect. How license keys work covers the tradeoffs.
7. Your server actually failed. The symptom is several unrelated customers reporting the same thing in the same hour.
The confirming question: is this one ticket or three? Volume is the tell. One failure is almost never infrastructure. A cluster always is. Watch your validation failure rate by reason for exactly this, which is one of the licensing metrics worth tracking.
Normalize the input before you validate it
Most copy and paste failures disappear if you clean the string before checking it. This is a small function and it eliminates a large share of these tickets.
// Run this on every key the user gives you, before any validation.
// Cheap, boring, and removes an entire category of support ticket.
export function normalizeLicenseKey(raw: string): string {
return raw
// 1. Curly quotes and dashes inserted by mail clients and word processors.
.replace(/[‘’‛′]/g, "'")
.replace(/[“”″]/g, '"')
.replace(/[‐-―−]/g, '-')
// 2. Every kind of whitespace, including non-breaking spaces and the
// line breaks a mail client added when it wrapped the key.
.replace(/[\s ]+/g, '')
// 3. Wrapping punctuation people paste along with the key.
.replace(/^["'<(\[]+|["'>)\]]+$/g, '')
// 4. Case and separators. Store keys uppercase and compare without dashes.
.toUpperCase()
.replace(/-/g, '')
// 5. Visually ambiguous characters, only if your alphabet excludes them.
// Do this ONLY if O, I, and the digit 1 are not valid in your keys.
.replace(/O/g, '0')
.replace(/[IL]/g, '1')
}
Step five is the one to think about. Substituting O for 0 is safe only if your key alphabet never contains both, which is an argument for using a restricted alphabet like Crockford Base32 when you design the format in the first place. If your keys can contain both characters, delete that step rather than guessing.
Do the same normalization on the server. A client that cleans input and a server that does not will disagree the moment someone activates through a different path, and that disagreement is a very confusing ticket.
Make the app name the reason
Replace “Invalid license key” with the actual reason. This is the single change that removes the most tickets, and most apps never make it.
The customer cannot see your database. When your app says “invalid,” they infer the only thing they can see, which is the string they typed, so they retype it, get the same error, and email you. Meanwhile the real cause was a used-up device limit, which no amount of retyping will ever fix.
Write a distinct message for each branch:
- Key not recognized: “We could not find this key. Check that it is from the purchase email for this app.”
- Device limit reached: “This license is already active on 3 of 3 devices. Remove one to continue.”
- Revoked: “This license is no longer active. Contact support if you believe this is a mistake.”
- Lease expired: “We need to reconnect to verify your license. Check your internet connection.”
- Server unreachable: “We could not reach the licensing server. Your license still works, and we will retry.”
Note that the last two are not the customer’s fault and should not be phrased as if they are. And when the server is unreachable, do not lock the app. Failing closed on your own outage turns an infrastructure blip into a wave of tickets from people who paid you.
Then add a copyable diagnostic code next to the error: reason, timestamp, and a truncated device identifier, something like DEV_LIMIT-20260817T1432Z-9f3a. It is not for the customer to interpret. It is so their first email contains the answer instead of a guess.
Where the reason codes come from
You have two paths and both are legitimate.
Building this yourself means storing a reason on every failed validation, keeping enough activation history to answer “which machine took that seat and when,” and surfacing all of it in an internal view you can search by email. That is a real weekend of work plus the maintenance, and the trap is that it never feels urgent until a customer asks a question your data cannot answer.
The alternative is a licensing layer that records the reason by default, because it has to know all of it anyway to make the decision. Keylight returns a specific reason on every failed activation and validation, keeps the device list per license, and exposes both to you and to the customer. Stripe stays your payment processor and owns the money side. See pricing for what the licensing side costs.
Whichever path you take, the test is the same. Take your last five activation tickets and ask whether you could have answered each one without a reply. If the answer is no, the problem is not your support process. It is that your app is keeping the reason to itself.
If there is a failure mode here I missed, send us your feedback.
Frequently asked
Why is my license key not working?+
In order of how often it happens: the email never arrived, the key was damaged by copy and paste, the device limit is already used up by an old machine, the offline lease expired, or the key was revoked after a refund. A generic "invalid license" error cannot tell these apart, which is why the app should name the reason.
What causes license activation to fail after copy and paste?+
Trailing whitespace, non-breaking spaces, curly quotes inserted by the mail client, and line wrapping inside the email body. Normalizing the input before validating removes most of these failures without the customer noticing anything happened.
What is the single question to ask a customer who cannot activate?+
Ask for the exact error text the app displayed, word for word, plus the email address used at checkout. Those two facts eliminate most branches of the triage tree in one reply.
Should an app show a diagnostic code on activation failure?+
Yes. A short copyable code that encodes the reason, the timestamp, and a truncated device identifier turns a three-email exchange into one. It also stops customers from guessing at causes that are not theirs to diagnose.
Ready to ship?
Create your account and start licensing your apps in under a minute. Free forever tier included.
Start Free