Designing API authorization means answering, for every request, whether this specific actor may perform this specific action on this specific resource — a question that is entirely separate from proving who they are. A scope model that stays sane as your API grows splits read from write, picks granularity deliberately, and treats organization, user, and resource as distinct dimensions instead of one flat permission list.

Quick answer: Authorization is a three-part question — which actor, which action, which resource. Design scopes around that triad, split read from write, name them with a convention you won't outgrow, and deprecate them on the same public timeline you'd use for any breaking API change.

Authentication vs. Authorization: Different Questions, Different Failure Modes

Authentication (authn) verifies identity: the token is valid, the signature checks out, this really is the caller it claims to be. Authorization (authz) is a separate decision made after that — given this verified identity, is this exact action on this exact resource permitted? Conflating the two is why so many partner APIs ship an all-or-nothing model: everything a valid token holder could possibly need, and nothing less.

The failure mode is predictable. Middleware parses the JWT, validates the session, confirms the caller is real — and then, because nobody built a second, distinct decision point, the endpoint just... proceeds. Permission logic gets smuggled in later as scattered if user.role == 'admin' checks, duplicated across handlers, drifting out of sync with each other.

OWASP's API Security Top 10 has flagged broken object-level authorization (BOLA) as the most common critical API vulnerability across multiple survey cycles — not because authentication was weak, but because authorization was never modeled as its own explicit step. The token was valid; nobody checked whether the token's owner should see that particular record.

Good authorization systems share three properties:

  • Authorization is an explicit decision, not something that falls out of "the token parsed successfully."
  • Every endpoint documents its required scope(s) as part of its contract, not as a comment buried in the handler.
  • Scope checks happen at one enforcement point per request, not scattered across a dozen conditionals.

That last point matters for process, not just code. The permission model for an endpoint is as much a part of its contract as the request schema — which is why the discipline of specifying an API contract as a PM's actual job has to include required scopes, not leave them to whichever engineer implements the route first.

Scope Granularity: Coarse, Fine, and Picking the Right Altitude

Coarse scopes (read, admin) minimize consent-screen friction and are trivial to reason about, but grant far more access than most individual integrations need. Fine scopes (invoices.read.own) enforce least privilege precisely, at the cost of multiplying how many scopes partners must request and you must maintain forever. The right altitude depends on a resource's blast radius, not on habit.

DimensionCoarse scopesFine-grained scopes
Exampleread, write, admininvoices:read:own, payouts:write:org
Consent-screen frictionLow — one or two linesHigher — a long list unless bundled
Least-privilege fitWeak — over-grants by defaultStrong — matches actual need
Maintenance burdenLowGrows with every new resource/action
Best forInternal tools, early-stage APIs, low blast-radius dataRegulated data, partner APIs, high-value resources
Biggest riskOver-privileged tokens, hard-to-audit accessScope explosion (see below)

Four factors should drive where you land on that spectrum:

  1. Blast radius — what's the worst case if a token with this scope leaks? Financial and health data justify finer scopes than a public read-only catalog.
  2. Number of distinct consumer use cases — one integration pattern tolerates a coarse scope; a dozen different partner archetypes usually need to be told apart.
  3. Regulatory requirements — frameworks like SOC 2 and HIPAA expect access to be demonstrably minimal, which is hard to show with a single admin scope covering everything.
  4. How fast your resource model grows — fine-grained scopes tied 1:1 to every resource become unmaintainable if you ship new resource types monthly.

Your protocol choice interacts with this decision more than most teams expect. REST endpoints map cleanly to resource-plus-action scopes because a request already targets one resource type. A single GraphQL query, by contrast, can touch a dozen resource types in one round trip, which blurs where a scope check even attaches — one more reason enforceability deserves a seat at the table in the decision between REST, RPC, and GraphQL, not just the usual latency and tooling tradeoffs.

Three Axes of Every Scope: Org, User, and Resource

Every scope decision resolves along three independent axes: which organization's data, which user's own data within that organization, and which specific resource or resource type. Treating these as one flattened dimension is exactly why permission bugs happen — a scope that grants org-wide read access looks identical, in a quick code review, to one scoped to the caller's own records.

AxisQuestion it answersExample scopeTypical enforcement point
OrgWhich tenant's data is in play?invoices:read:orgMiddleware, tenant-id resolved from token
UserWhose records within the org — anyone's, or just the caller's?invoices:read:ownRow-level check against owner_id
ResourceWhich resource type, or which specific instance?invoices:read, invoices:read:{id}Route/handler-level check

The org axis deserves a special warning: never rely on scope naming alone to enforce tenant isolation. A scope tells you what an actor is permitted to request; it does not, by itself, filter which tenant's rows a query returns. That filter has to be enforced independently, every time, regardless of what the scope is called — treating a well-named scope as a substitute for an actual ownership check is how cross-tenant data leaks happen.

Google's 2019 Zanzibar paper — the design behind unified authorization across Google's own products — models every permission check as a relationship between a subject and an object, which is effectively this same triad formalized into a queryable graph. You don't need Zanzibar-scale infrastructure to borrow its discipline: name the actor, the object, and the relation explicitly, rather than letting them stay implicit in code.

Modeling the resource axis well starts upstream, with the same nouns you used when you modeled your API's resources in the first place — a scope named after a resource that doesn't exist in your API's noun list is a sign the scope model and the resource model have drifted apart.

A Scope-Naming Convention That Scales

A scope name should encode resource, action, and qualifier in a fixed, predictable order — for example resource:action:qualifier, as in invoices:read:own or invoices:write:org — so a developer can infer what a scope grants without opening documentation. Which exact convention you pick matters less than picking one and never deviating from it.

SegmentMeaningExample values
resourceNoun from your API's resource modelinvoices, users, webhooks
actionVerb — what the scope permitsread, write, admin
qualifierBreadth — org-wide, own-only, or a specific instanceorg, own, omitted (defaults to org)

Applied consistently, that convention reads like a sentence:

  • invoices:read:own — read only the requesting user's own invoices
  • invoices:read:org — read every invoice across the organization
  • invoices:write:org — create or update invoices, organization-wide
  • webhooks:admin:org — manage webhook configuration for the organization

Always split read from write, even for a resource where you currently only expose one combined scope. Bundling them feels harmless on day one; unbundling later means every partner holding the combined scope must re-consent, which is a breaking change you'll want to avoid triggering twice.

This isn't a theoretical convention. GitHub's own OAuth App scopes follow the same logic: repo is deliberately coarse, while read:org and write:org split the organization-data path so an integration can request exactly the half it actually needs. Stripe's restricted API keys go further, letting a builder choose, resource by resource, whether a key can read, write, or do neither — a concrete, shipped implementation of the same org/user/resource triad, not just an abstract principle.

Naming discipline is one piece of a larger practice. It belongs alongside the versioning, deprecation, and documentation habits covered in a complete guide to API product design — a scope convention designed in isolation from those habits tends to rot the fastest.

Scope Explosion: The Failure Mode Nobody Plans For

Scope explosion happens when granularity keeps splitting along every axis — resource × action × qualifier — until an API exposes hundreds of scopes that no partner, and often no internal engineer, can reason about anymore. Twenty resources times three actions times three qualifiers is already 180 scopes. The fix isn't retreating to one coarse admin scope; it's grouping scopes around the jobs partners actually hire your API to do.

Watch for these warning signs:

  • Partners requesting "just give me admin" because the fine-grained list has become unusable
  • A consent screen listing 40+ individual line items
  • Support tickets asking "which scope do I actually need for X"
  • New engineers copy-pasting an existing scope list instead of reasoning through what an endpoint needs

Rather than deriving scopes purely from CRUD-on-nouns, look at the actual jobs a partner integration is hired to do. A payroll integration doesn't want a scope named after a database table — it wants "process a payroll run," which happens to require read access on three resources and write access on one. Framing scopes around outcomes, the same discipline behind a jobs-to-be-done analysis, keeps your scope count proportional to real integration patterns instead of your schema's raw cardinality.

To prevent or recover from explosion:

  1. Audit actual usage — pull which scopes real partners request together, not which combinations are theoretically possible.
  2. Introduce composite scopes that bundle the resource/action pairs a common job actually needs, and market those as the default option.
  3. Cap qualifier granularity to org and own for most resources; reserve per-instance scopes for cases with a genuine regulatory reason.
  4. Sunset unused fine-grained scopes through the same deprecation process you'd use for any other breaking change (below).

Deprecating a Scope Without Breaking Every Integration

Deprecating a scope safely means never deleting it the moment a replacement ships. Instead, run the old and new scopes in parallel, announce a firm sunset date, monitor which partners are still presenting the old one, and only enforce removal after the notice window has actually passed. Scopes are part of your API's public contract, so they deprecate on the same timeline discipline as an endpoint or a field.

A workable sequence:

  1. Ship the replacement scope alongside the old one; both grant equivalent access during the transition window.
  2. Announce deprecation with a firm date, not an open-ended "eventually" that never arrives.
  3. Log every request that still presents the old scope, so you have real data on who hasn't migrated.
  4. Surface a warning to partners still using it — a response header, a dashboard notice, a direct email if the volume justifies it.
  5. Enforce removal only once usage has dropped to near zero, or the announced date passes, whichever discipline you committed to publicly.

This is exactly the pattern covered in versioning an API without breaking integrations — removing a scope is a breaking change wearing a different hat, and it deserves the same advance-notice contract you'd apply to removing a field or retiring an endpoint outright.

Treating Scopes as Part of the Reviewable Contract

It doesn't make the judgment calls in this article for you. It just makes them checkable.

Key Takeaways

  • Separate authn from authz explicitly. Verifying identity and deciding what a verified identity may do are two distinct steps; conflating them is how all-or-nothing permission models happen.
  • Pick granularity by blast radius, not habit. Coarse scopes suit low-risk, internal, or early-stage APIs; fine-grained scopes suit regulated or high-value resources.
  • Model org, user, and resource as three separate axes. A scope's name can look identical for org-wide and self-only access if you collapse these dimensions into one.
  • Never let scope naming substitute for a real tenant-isolation check. Enforce ownership at the query level regardless of what the scope is called.
  • Adopt one fixed naming convention — such as resource:action:qualifier — and never deviate from it once partners depend on it.
  • Split read from write scopes from day one, even if you currently issue them together; unbundling later forces every partner to re-consent.
  • Design scopes around jobs partners are hiring your API to do, not raw CRUD-on-nouns, to keep the total count from multiplying uncontrollably.
  • Deprecate a scope on a public timeline — parallel run, announced date, usage monitoring, then enforcement — the same discipline as any other breaking API change.

Frequently Asked Questions

What's the difference between authentication and authorization in API design?

Authentication confirms who is making the request — a valid token, a verified identity. Authorization is a separate, later decision about whether that verified identity may perform a specific action on a specific resource. An API can have airtight authentication and still leak data if authorization was never modeled as its own explicit step.

How many OAuth scopes should an API have?

There's no universal number — it should track the distinct jobs your partners actually do, not the mathematical product of every resource, action, and qualifier your schema allows. As a signal, if partners keep requesting broader access than the fine-grained list technically requires, or support tickets ask which scope covers a common task, you likely have more scopes than your integration patterns justify.

Should I use coarse or fine-grained scopes for a partner API?

Match granularity to blast radius: coarse scopes for low-risk, internal-style access; fine-grained scopes for regulated or high-value resources like payments or health data. Most mature partner APIs land in between — coarse composite scopes built around common jobs, with fine-grained options available for partners who specifically need them.

How do you deprecate an OAuth scope without breaking integrations?

Run the old and new scopes in parallel, announce a firm sunset date, log which partners still present the old scope, warn them directly, and enforce removal only after the notice window passes. Treat scope removal as a breaking change with the same advance-notice contract you'd use for retiring an endpoint or a field.

What is scope explosion and how do you prevent it?

Scope explosion is when granularity multiplies across every axis — resource times action times qualifier — until an API has hundreds of scopes nobody can reason about. Prevent it by grouping scopes around actual partner jobs-to-be-done, capping qualifiers to org/own by default, and auditing real usage patterns rather than generating every theoretically possible combination.