API keys are one of the most copied-and-pasted credentials in software. We rebuilt ours from scratch, and every one of the 69 characters exists for a reason.
Here's a Paddle API key:
pdl_live_apikey_01gtgztp8fpchantd4g1wrksa3_KAmN5c5zxXJtM8xNra4EFt_AQOIt's 69 characters long. If you've ever created one in the Paddle dashboard, you probably copied it into an environment variable and moved on with your day. Fair enough — that's what you're supposed to do with it.
But this string is the result of a lot of deliberate design decisions. Every segment, separator, and character is there for a reason. Some of our design choices are about security, some are about performance, and one is simply about making the key easy to copy.
This is the story of how we designed it.
Why we rebuilt our keys
For most of Paddle's history, API keys were 50-character alphanumeric strings. They lived in our monolith, they looked like every other random credential on the internet, and they worked fine, but the problems stacked up gradually:
No way to identify it as a Paddle key.
Without a prefix or structure, secret scanning services had nothing to match on. If a key ended up in a public repository, there was no way to detect it as ours.No way to tell which environment it was for.
Sandbox and live keys were identical in format. Grab the wrong one, point it at production, and you'd only find out when real payments started flowing — or when they didn't.Authentication was slower than it needed to be.
There was no public identifier to go straight to the right record. Every request meant finding the key by its secret alone, inside the monolith. At our request volumes, that added up.
We'd been thinking about these problems for a while. Building the new API Key Service, which moves authentication out of our monolith into a dedicated Go service, gave us the chance to fix them at the source. Beyond technical choices, like where a key is stored and how it's managed, we could change what it looks like as a string.
What we needed the key to do
Before designing the format and defining a feature set, we asked ourselves:
What are the properties that a well-designed credential should have?
We wrote these down:
Identifiable.
You should be able to tell that a string is a Paddle key at a glance. Not by looking it up, but by looking at it.Self-validating.
Third-party services should be able to confirm it's a genuine Paddle key without calling our API every time.Safely observable.
Parts of the key should be displayable in dashboards, logs, and API responses without exposing the secret.Fast to authenticate.
There has to be enough structure to locate the key by primary key, not by hash scan.Future-proof.
We should be able to change hashing algorithms or validation logic without changing the format itself.Developer-friendly.
The key should be easy to copy and read. Small DX choices like that seem trivial on their own but compound over time.
These six requirements shaped every decision that followed.
Anatomy of the key
Here's the full key broken down into its core components:
pdl_live_apikey_01gtgztp8fpchantd4g1wrksa3_KAmN5c5zxXJtM8xNra4EFt_AQO
- Platform prefix
pdlIdentifies this string as a Paddle credential. - Environment
livelivefor production,sdbxfor sandbox. - Resource type
apikeyDistinguishes credential types, so new key types can be added later. - Unique ID
01gtgztp8fpchantd4g1wrksa3The unique part of the Paddle ID. Together with the resource type, it's the public identifier and database primary key. - TokenSecret
KAmN5c5zxXJtM8xNra4EFtThe secret, stored only as a salted SHA-512 hash. - Version
AIdentifies the hashing and validation algorithm in use. - Checksum
QOFletcher-16 check that catches malformed keys without a network call.
Rather than walking left to right, let's look at this from the lens of the problems each piece solves.
Making the key identifiable
The first thing you notice about a Paddle API key is the four-character pdl_ prefix. It's a small detail, but it's what let us partner with GitHub on secret scanning.
GitHub scans repositories for leaked credentials by matching strings against regex patterns provided by each partner. Our old keys were 50 random alphanumeric characters, indistinguishable from any other random string, so there was nothing to match on.
With the pdl_ prefix in place, we enrolled in GitHub's secret scanning partner program and submitted a pattern for each environment:
Production: ^pdl_live_apikey_[a-z\d]{26}_[a-zA-Z\d]{22}_[a-zA-Z\d]{3}$Sandbox: ^pdl_sdbx_apikey_[a-z\d]{26}_[a-zA-Z\d]{22}_[a-zA-Z\d]{3}$The pattern is specific enough to avoid false positives but broad enough to catch real keys.
GitHub uses these to find candidate strings, then sends them to our dedicated secret scanning service for validation. We built that as a separate, lightweight Go service for two reasons:
- GitHub authenticates its requests in a non-standard way.
- We didn't want to pollute our existing auth infrastructure.
After the prefix comes the environment: live_ for production, sdbx_ for sandbox. This helps customers prevent mistakes in the storage and usage of keys across their own environments simply by looking at the key. It also covers internal keys. Development, local, and staging environments each have their own identifier.
A self-describing key does have one trade-off: it tells an attacker exactly which platform it belongs to. We weighed this during design and decided the benefit of automated detection and revocation far outweighs the marginal advantage of obscurity. Within the first few weeks of shipping secret scanning, we identified 8 customers with exposed production keys and 6 with exposed sandbox keys, and that was before we'd even switched on automatic revocation. A clear win.
The public/private split
With legacy keys, the entire string was the secret. Every character played the same role: authenticate the request. That's fine until you need to identify the key in the dashboard, reference it in logs, or tell one key from another.
The new format splits the key into two components with fundamentally different roles.
The Paddle ID (
apikey_01gtgztp8fpchantd4g1wrksa3) is the public component. It's the primary key in our database. It appears in the dashboard, in API responses, in logs. It's an identifier, not a secret, so it can be shared and stored in plaintext.The token (
KAmN5c5zxXJtM8xNra4EFt) is the secret. It's 22 cryptographically random alphanumeric characters, hashed with SHA-512 and a random salt before storage. We chose that over a password hash like Argon2 on purpose: a key is validated on every request, and a deliberately slow hash would add latency to every API call. The hash only has to slow down someone who has already stolen the database, and a 22-character random token has far more entropy than any password. It's shown once, when you create the key. We only keep the hash, so we can't show it to you again.
var charset = []byte("0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz")
func (c *generator) generate(size int) Token { return Token(uniuri.NewLenChars(size, charset))}The split also helps performance. Because the Paddle ID is embedded in the key, our authentication service extracts it and reads the row by primary key, instead of matching hashes across the whole table.
The old system had only the secret to go on, which is much slower at high request volumes. Moving validation into the new service alone cut authentication latency by 75%, and that was for legacy keys, which still have to be looked up by hash. New-format keys skip the hash lookup entirely.
Self-validation without calling home
The last three characters of the key (AQO) pack in more than you'd expect.
The first character is the version: a single uppercase letter, deliberately not a number. We went back and forth on this, but numbered key versions would have invited questions like "Is this a version 2 key?" or "Does it work with API version 1?" A letter can't be mistaken for an API version.
The version is there for future-proofing. If we change our hashing algorithm or validation logic, we increment the version and the service knows which path to take: old keys keep working, new keys use the updated process, and nobody has to migrate.
Every Paddle key is currently version A, and it'll probably stay that way for a long time. But when the day comes — and in security, it always does — we won't have to change the format itself.
The second and third characters are the checksum. We use a Fletcher-16 algorithm computed against the Paddle ID:
func fletcher16Alnum(data string) string { sum1 := 0 sum2 := 0 mod := 255
for _, char := range data { sum1 = (sum1 + int(char)) % mod sum2 = (sum2 + sum1) % mod }
alnumChars := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" base := len(alnumChars)
char1 := rune(alnumChars[sum1%base]) char2 := rune(alnumChars[sum2%base])
return string([]rune{char1, char2})}The checksum serves two purposes:
- It rejects invalid keys before hitting the database.
If the checksum doesn't match, the key isn't ours. - It lets our secret scanning service reject false positives cheaply.
GitHub only matches the pattern. Every candidate it sends us is parsed and checksummed first, so a string that merely looks like a key never reaches the API Key Service.
The checksum covers the Paddle ID only, not the full key. We originally considered checksumming everything, but narrowed the scope after review. Running it only on the ID casts a wider net. If someone tampers with the environment segment, the checksum still holds and the key still gets flagged. Full validation still happens server-side, and we'd rather flag a key and investigate than silently dismiss it.
The details that compound
Some decisions are purely about making the key nicer to work with, not security or architecture.
Underscores, not dashes.
On most systems, double-clicking selects text between word boundaries. Dashes are word boundaries; underscores aren't. Double-click anywhere on a Paddle API key and the entire 69-character string gets selected. Copy, paste, done.Consistent length across environments.
Keys are always 69 characters, sandbox or live. We usesdbx(four characters) instead ofsbx(three) because it matches the length oflive. Consistency matters for pattern matching and the quick "does this look right?" check.An 80-character ceiling.
We set an 80-character ceiling early on: enough room for everything we needed, but short enough to display in a terminal, fit in a config field, and not wrap awkwardly in a text editor. The final format came in at 69.
What the rebuild enabled
The format is the visible part of the rebuild. Underneath it, every key now has an identity of its own, and that's what makes giving it permissions, an expiry, and a lifecycle possible.
Granular permissions
Legacy keys had full access to everything. With the new system, every key has entity-based permissions (transaction.read, subscription.write, discount.read) mapped directly to the Paddle Billing API. Write implies read, but not the reverse.
New keys start with no permissions. You choose what to grant, and a key can never have more access than the person creating it. The secure path is the default.
Immutable expiry
Keys default to a 90-day expiry, inspired by similar presets on platforms like GitHub. The API allows custom dates up to a year out, or no expiry at all. The dashboard shows a clear warning if you pick no expiry.
Expiry dates can't be edited. Our security team was firm on this: if you can extend the life of a key after the fact, you've undermined the reason for setting an expiry. The only way to get a later date is to rotate the key, which replaces the secret behind it. The new date only takes effect once the new secret is in use.
Revocation with a safety net
Revoked keys can be reactivated within 60 minutes. After that, revocation is permanent. We added the window after someone accidentally revoked a key embedded in their deployment pipeline and took their integration down. An hour is enough time to notice and undo it.
Keys that Paddle revokes through secret scanning can't be reactivated at all. The exposure has already happened, so the key stays compromised either way.
Secret scanning
This is where the format paid off most directly. The pdl_ prefix and checksum made the GitHub partnership possible. Active keys found in public repositories are automatically revoked, with the account owner notified of the exact location (repository, commit, or file). GitHub also uses the same pattern for push protection, so a commit containing a Paddle key is blocked before it reaches a public repository.
People have always pasted keys into code by mistake. As more of that code is written by AI, or vibe-coded entirely, more keys end up in commits by accident. That makes this kind of automated detection essential.
Evolving by design
The key format was built to evolve:
- The version character means we can change cryptographic primitives without breaking existing keys.
- The
apikey_resource type means we can introduce new key types. - Secret scanning is designed to take partners beyond GitHub. The validation service was built for scanning partners in general, and GitHub is the first.
Rotation is no longer manual, either. Rotating a key used to mean creating a new one by hand and revoking the old one. That's friction that discourages good security hygiene at scale. Now a rotatable key can mint a new secret with a configurable grace period of up to 24 hours, so both old and new credentials stay valid while you switch over. Because the key's ID never changes, only the secret, AWS Secrets Manager can rotate Paddle keys stored there natively, on a schedule you set. The same rotation endpoints are public, so you can rotate keys with the Paddle API from your own tooling.
All of this builds on the format. A structured key with a public/private split and self-validation is easier to secure, and easier to build on.
Final thoughts
An API key looks simple: you paste it into an environment variable and forget about it. But it touches authentication, authorization, performance, partner integrations, and developer experience.
Treating it as a designed artifact instead of a random string changed how we think about credential security at Paddle. Each of those 69 characters carries a decision: some obvious, some debated for weeks, and some we're still iterating on.
If you're designing credentials for your own platform: start with the format. Everything else builds on it.

About David Grayston
David Grayston is a Senior Software Engineer in the Developer Experience team at Paddle. He looks after API keys, webhooks, SDKs, and the remote MCP server, making Paddle easier to build on.