Generate API keys with 128 to 512 random bits (122 for UUID v4), in the format your system expects. Each key comes with its SHA-256 hash in the second column: the key goes to your user once, and the hash is what belongs in your database.
hex (32 bytes) 4f1c0e9a7d2b...e83a (64 hex chars, 256 bits)
base64url sk_live_uLha-a4ksExdU0PxS_3AReyk2hjtKo7Z5eTpuSYhwCg
base62 sk_live_3B9uYL9oSV7OgjIKZEmRZIZEUnqYi0jDECYWwgKfq3I
UUID v4 1e27e956-52fc-4d8e-b165-082925499078
prefix + checksum myapp_Gl22VAKLFtpJgrrRE1WMxrmaUNaHhuDg9A0Vf3bt5Ej2YmHfL myapp_ Gl22VAKLFtpJgrrRE1WMxrmaUNaHhuDg9A0Vf3bt5Ej 2YmHfL
^ ^ ^
| random body, 43 base62 chars (256 bits) CRC32(body) in base62, 6 chars
prefix: who issued it, which environment
This follows the layout GitHub moved to in 2021 for its tokens
(ghp_, gho_ and friends): a short prefix
that names the token type, a random base62 body, and a CRC32
checksum of the body in the last six characters. The prefix makes leaked keys easy to grep for and lets secret scanners
match them with very few false positives. The checksum lets your API reject
a mistyped or truncated key without a database lookup, and lets a
scanner confirm a match offline.
Here the checksum is CRC32 (the zlib polynomial) of the body only, not the prefix, written as base62 with the digits-first alphabet (0-9A-Za-z) and left-padded to 6 characters. The Check box at the bottom recomputes it. CRC32 detects typos but adds no security: all of the key's strength comes from the random body.
Show the key to the user once, save sha256(key), and
on each request hash the incoming key and look up the hash. A database leak then exposes nothing usable.
SHA-256 is enough for API keys.
Unlike passwords, they do not need bcrypt or Argon2: with 128 or
more random bits there is nothing to brute force, and a fast hash
keeps the lookup cheap on every request. Store the prefix and the
last four characters in plain text if you want to show users which
key is which.
Keys are generated with crypto.getRandomValues() and
hashed with crypto.subtle in this tab. Nothing is sent anywhere. For production keys, we recommend
generating them on the server that stores the hash, so the key
never passes through a clipboard.
const crypto = require("node:crypto");
const zlib = require("node:zlib");
const B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
function base62(len) {
let out = "";
while (out.length < len) {
for (const b of crypto.randomBytes(len * 2)) {
if (b < 248 && out.length < len) out += B62[b % 62]; // 248 = 4 x 62, no bias
}
}
return out;
}
function toBase62(n, width) {
let s = "";
do { s = B62[n % 62] + s; n = Math.floor(n / 62); } while (n > 0);
return s.padStart(width, "0");
}
function newKey(prefix) {
const body = base62(43); // ~256 bits
const check = toBase62(zlib.crc32(body), 6); // zlib.crc32 needs Node 20.15+
return prefix + body + check;
}
const key = newKey("myapp_");
const hash = crypto.createHash("sha256").update(key).digest("hex");
console.log(key); // give to the user once
console.log(hash); // store this
// Simple formats
console.log(crypto.randomBytes(32).toString("hex"));
console.log(crypto.randomBytes(32).toString("base64url"));
console.log(crypto.randomUUID()); On lookup, hash the incoming key and query by hash. Compare with crypto.timingSafeEqual if you compare in application code rather than in the database.
import hashlib
import secrets
import zlib
B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def to_base62(n, width):
s = ""
while True:
n, r = divmod(n, 62)
s = B62[r] + s
if n == 0:
return s.rjust(width, "0")
def new_key(prefix):
body = "".join(secrets.choice(B62) for _ in range(43))
return prefix + body + to_base62(zlib.crc32(body.encode()), 6)
def checksum_ok(key):
rest = key.rsplit("_", 1)[-1]
return to_base62(zlib.crc32(rest[:-6].encode()), 6) == rest[-6:]
key = new_key("myapp_")
print(key, checksum_ok(key))
print(hashlib.sha256(key.encode()).hexdigest())
print(secrets.token_hex(32)) # 64 hex chars
print(secrets.token_urlsafe(32)) # base64url, 43 chars <?php
const B62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
function toBase62(int $n, int $width): string
{
$s = '';
do {
$s = B62[$n % 62] . $s;
$n = intdiv($n, 62);
} while ($n > 0);
return str_pad($s, $width, '0', STR_PAD_LEFT);
}
function newKey(string $prefix): string
{
$body = '';
for ($i = 0; $i < 43; $i++) {
$body .= B62[random_int(0, 61)];
}
return $prefix . $body . toBase62(crc32($body), 6);
}
$key = newKey('myapp_');
echo $key, PHP_EOL;
echo hash('sha256', $key), PHP_EOL; // store this
echo bin2hex(random_bytes(32)), PHP_EOL; // plain hex key crc32() returns an unsigned value on 64-bit PHP, which is what toBase62() expects. Use hash_equals() when comparing hashes yourself.
Entropy is what makes a key unguessable, and the format only changes how it is written. 32 random bytes is 256 bits whether you print it as 64 hex characters, 43 base64url characters or 43 base62 characters. The checksum adds 32 bits of length but zero bits of security, since anyone can compute a CRC.
This tool follows the specifications below. Read them when you need the exact normative wording, for example to settle an edge case between two implementations.
At least 128 bits of randomness, and 256 is a comfortable default. That is 32 hex characters or 22 base62 characters for 128 bits. The entropy of the current format is shown above the list.
Yes. Store the SHA-256 of the key and compare hashes on each request, so a database leak does not hand out working keys. A fast hash is fine here because a random 256-bit key cannot be brute forced, unlike a password.
A short prefix naming the issuer, a random base62 body, and a 6-character base62 CRC32 of the body at the end. It follows the layout GitHub adopted for its tokens: the prefix makes leaks easy to scan for, and the checksum catches typos without a database lookup.
A UUID v4 from a secure generator has 122 random bits, which is enough to be unguessable. It is not self-describing, though, and many UUID libraries in the wild are not guaranteed to use a CSPRNG, so a prefixed random key is the better default.
No. They come from crypto.getRandomValues() in your browser and the hashes from crypto.subtle; the page sends nothing. For production, generating keys on the server that stores the hash keeps them out of the clipboard entirely.
To sign requests with a shared secret instead of sending it, use the HMAC generator. The SHA generator hashes any text, and for human passwords use the password generator and store them with the bcrypt generator.