Designing API Account-Level vs Key-Level Scoping: When Each Tier Belongs
Most API designs collapse the account-vs-key distinction into one tier and then struggle to add the other tier later. The choice matters more than it looks: it determines what audit logs show, how rate limits divide, how customers organize CI vs production credentials, and what happens during inc...
A lot of B2B SaaS APIs start with a single tier of credentials. A customer signs up, gets one API key, and uses it. The early-stage logic is correct: one tier is simpler to implement, simpler to document, and adequate for the first hundred customers. The trouble is that the structural choice quietly determines how the API behaves at scale, and the migration from one tier to two is one of the harder migrations to do without breaking customer integrations. The choice deserves more thought up front than most API designs give it.
The two-tier model has an account at the top (a single organization, billing entity, or workspace) and one or more keys beneath it (separate credentials each representing a use site). Every key belongs to exactly one account. Authentication is on the key. Authorization, billing, audit, and most policy decisions are on the account. The clean version of this model appears in Stripe (publishable vs secret keys, restricted keys, account-level dashboards) and AWS (IAM users and roles under one account). The collapsed version (one credential per customer) appears in many smaller APIs, and is the more common starting point because it is what one writes when not thinking about the structural choice.
What account-level scoping buys you
Five behaviors get easier with explicit accounts. First, billing is at the account: usage from all keys aggregates into one invoice, plan tier is one row in the account record, payment method is one association. Without accounts, billing keys to a credential, which fails the moment a customer creates a second one. Second, audit aggregates at the account: the question "what did this customer do last week" has a clean SQL answer of WHERE account_id = .... With key-only, the question becomes a join across all known keys plus the problem of keys that no longer exist. Third, policy applies at the account: the per-account rate limit, the per-account allow-list, the per-account feature flag set, the per-account region pinning all become single-row reads instead of fan-out across keys. Fourth, incident response operates at the account: revoking all credentials, applying an emergency rate limit, putting an account into maintenance mode all become atomic operations. Fifth, customer self-service operates at the account: rotation of one key without disrupting others, separate keys for CI and production and staging without separate signups, separate keys per developer for incident attribution, all require the account as a parent.
What key-level scoping buys you
Three behaviors get easier with explicit keys. First, blast-radius limitation: a compromised key affects only its use site, not the entire account. The rotation procedure swaps one key, not all credentials. Second, attribution: server logs show which key made a request, which means "the staging environment is making production calls" becomes a visible bug rather than an invisible one. Third, differential scoping: a key in CI can have read-only permission, a key in production can have full permission, a key handed to a customer's contractor can have a single-resource scope. Without per-key scoping, all three use sites have the same authority.
The interesting observation is that the key benefits compose with the account benefits. Account-level billing plus key-level attribution is the right combination. Account-level rate limits with key-level scoping is the right combination. The choice that comes up in practice is not between account-only and key-only but between two-tier and one-tier, and the two-tier model dominates.
The migration trap
The reason this matters at API-design time is that the migration from one tier to two is a breaking change for customers. The original credential's identifier is now ambiguous: is it the account ID or the key ID? The original endpoint that returned account-scoped data now needs to also accept key-scoped queries. The original webhook signature was account-keyed and now needs to be key-keyed for proper attribution. None of these changes can be done silently. Each requires a versioning story, a customer migration window, a deprecation pipeline, and the ongoing maintenance cost of supporting both tiers for the deprecation window's duration.
The pre-emptive design choice is to use a two-tier model from the start even if you only expose one key per account initially. The account ID is internal and stable. The key ID is the credential, prefixed and rotatable. Every API endpoint takes the key (the account is implicit through the key). Every database table that has a credential reference uses both the account ID and the key ID, even if the key ID is initially redundant. The customer-facing surface starts with one key per account; the second key per account becomes a feature you can add later without changing the model.
The naming convention question
Prefixed keys with role identifiers are the right default. Stripe uses sk_test_, sk_live_, pk_test_, pk_live_, rk_ for restricted. The prefix encodes mode, role, and intended visibility in two-to-three characters that customers can recognize at a glance. The convention also enables key-scanning tools (GitHub secret scanning, GitGuardian, Trufflehog) to identify credentials accurately and revoke them automatically when leaked. We use this convention across our four products, with dm_ for DocuMint, cp_ for CronPing, fb_ for FlagBit, and wv_ for WebhookVault.
The convention should include a hash of the rest of the key for the customer-visible last-four-characters display. Customers cannot meaningfully recognize "key ending in qZj7", but they can recognize "production key" if you let them name keys. The combination of a system-controlled prefix, a system-controlled suffix display, and a customer-controlled name is the right balance.
The scoping question
Per-key scopes are useful but unlimited scopes are usually not. The set of scopes should match the set of operations customers actually want to compose, which is usually a small set like read-only, read-write, billing-read, billing-write, admin. Trying to expose every endpoint as a separate scope produces a UX disaster and a security audit nightmare. The five-to-ten coarse scopes covering 95% of customer requirements is the right resolution. The remaining 5% can be served by per-key resource restrictions (this key can only access invoices, not customers).
The composition rule is usually intersection: a key can have a scope that is at most the account's plan permits. A free-tier account does not have webhook scopes available, so a key on that account cannot have a webhook scope even if requested. This rule is enforced server-side at key-creation time and re-checked at every authorization check, not relied on as a client-side discipline.
Three patterns that fail in production
The pattern of storing the account ID in the key itself via Base64 or a JWT structure looks clever and fails in practice. The problem is that key rotation becomes more difficult (the new key must encode the same account), and the prefix-and-suffix display convention breaks. Stick with opaque keys plus a server-side lookup table.
The pattern of shared keys across multiple accounts looks like an enterprise feature and fails in practice. The shared key has ambiguous attribution, ambiguous billing, ambiguous audit. The right pattern for enterprise sharing is per-account keys with optional cross-account access policies, not single keys with multi-account membership.
The pattern of generating new keys server-side and emailing them to customers is convenient and fails the secret-handling discipline. Keys should be returned in the API response that created them, displayed once in the dashboard, and never transmitted via channels that retain them. The email pattern is the most common origin of leaked credentials in our experience.
Why this matters for the studio
Our four products started with account-and-key tiers from the beginning (the account is the user record from signup; the key is what is returned and used for authentication). This was the right call. The migration cost we have avoided is not visible in the codebase, but it would have been measured in weeks of work and customer disruption. The cost of starting with the two-tier model was approximately zero (the additional schema column).
The deeper observation is that API designs have structural choices that look optional in the first hundred customers and become load-bearing by the first thousand. The credential model is one of those choices. The discipline of designing for the model you will need in two years rather than the model you need this week pays back in proportion to how successfully the API grows. The cost is small. The savings are real.