Your product issues API keys. The design of that key, as a string, determines whether a leaked one gets found before it is used, whether anyone can tell what it grants, and whether your customers can rotate without downtime.
Most keys are a random blob with no structure, because generating a random blob is the obvious implementation. A few small decisions about the format buy a great deal.
This post is what to put in the key and what to build around it. For whoever is designing the credential other people will paste into their systems.
Give it a recognisable prefix
A key that starts with a fixed, product-specific marker can be found by a scanner:
sk_live_9f3a1c7b2e5d4f60a8c1...
^^ ^^^^
| environment
kind (secret key)
This matters because of secret scanning. Source hosts and scanning tools match known credential formats, and several run partner programmes where a vendor registers their pattern and gets notified when a key appears in a public repository. A high-entropy string with no marker is invisible to all of it, and yours is the product that finds out last.
The prefix also prevents a class of support problem, because a person can look at a string and know what it is and which environment it belongs to.
Separate live from test explicitly in the prefix. Mixing them up is the most common customer mistake, and making it visible in the string removes most of it.
Add a checksum
A short checksum at the end lets anything holding the key verify it is well formed before using it:
sk_live_<random>_<checksum>
Two benefits. Your API can reject a malformed key without a database lookup, which removes a cheap denial-of-service path. And scanners get far fewer false positives, which makes them more willing to alert on your pattern.
Store the hash, show the value once
The key is a password. Store a hash, not the value, and show the plaintext exactly once at creation.
For fast verification, store an indexed lookup field alongside the hash: a short prefix of the key, or a separate identifier embedded in the key, so you can find the right row without scanning the table and then verify with a constant-time comparison.
Showing it once is a small user experience cost and it means a database read does not hand someone every customer's credentials.
Make each key describable
Every key should carry: a name the customer chose, who created it, when, what it is scoped to, when it was last used, and an optional expiry.
Last used is the field that makes everything else possible. Without it, a customer cannot tell which of their eleven keys is safe to delete, so they delete none, and an unused key from 2022 is still valid. With it, an access review of API keys is a sort by date.
Scopes should be narrower than the account. A key that can do everything the user can do is the default in most products and it is the wrong default: an integration that reads reports should not be able to delete them.
Expiry should be available and, for new keys, defaulted to something rather than never.
Rotation has to be possible without downtime
If a customer can hold only one key, rotation means an outage, so they never rotate.
Allow multiple active keys per integration. Then the procedure is: create the new key, deploy it, confirm traffic has moved by watching last-used, delete the old one. That is a sequence a customer can run on a Tuesday afternoon, and a single-key design makes it impossible.
Show them last-used per key so that middle step is verifiable rather than hopeful.
Tell them when something looks wrong
You can see things the customer cannot:
- A key used from a new country or a new network for the first time.
- A key not used for months that suddenly becomes active.
- A key that appears in a public repository, if you are enrolled in a scanning partner programme.
Notify the account, and offer immediate revocation from the notification. The partner-programme case is worth the integration effort on its own: it turns a leaked key into an email within minutes, which is faster than any detection you could build yourself.
When one leaks
Have the path ready before you need it: revoke immediately, tell the customer what the key could access and when it was last used from an unexpected source, and help them issue a replacement. Then check whether it was used by anyone else, and say so plainly either way.
The temptation to quietly revoke and say nothing is strong and wrong. A customer who learns later that you knew is a customer you have lost, and the notification is usually a relief rather than an accusation, because the leak was probably theirs.
The concession
All of this adds work to a feature that could be one random string in a column, and for an internal service used by two teams it is over-engineering. Structure, checksums and scanning partnerships are for products whose keys end up in other people's codebases.
The threshold is whether the credential leaves your organisation. Once a customer pastes it into their own repository, their CI and their scripts, you have lost control of where it travels, and everything above exists to make that manageable. Before that point, a random string and a hash are fine.
The implication
The key format is decided once, early, usually in an afternoon, and it is very hard to change afterwards because every customer has the old one in their systems.
Spend an hour on it. A prefix, a checksum, a stored hash, a last-used timestamp and support for two active keys are the difference between a credential you can help customers manage and one that quietly works forever.