An API is the one part of a system that was deliberately built to be called by software you do not control, which makes it a strange thing to threat model: the attack surface is not an accident or an oversight, it is the product. And unlike a user interface, an API offers no implicit constraints on how it gets used. A web form only submits the fields it renders, in the order the flow allows, after the screens that precede it. The endpoint behind that form accepts any field, in any order, at any time, from anyone who can reach it — and every assumption the interface was quietly enforcing has to be enforced again, explicitly, on the server, or it is not being enforced at all.
That is why an API threat model is organised differently from a system one. The data flow diagram still matters — it tells you where the API sits, what it reaches, and which boundaries it crosses — but the real work happens along a different axis: endpoints against objects, and callers against permissions. This guide is about that axis. It assumes the workshop mechanics are handled, and if the API is one service among many, threat modeling a microservices architecture covers the service-to-service half of the picture that this post deliberately leaves out.
The short version: Start from a real endpoint inventory, not the spec — they differ, and the difference is usually the first finding. Tier endpoints by exposure and model one representative of each tier properly instead of all of them shallowly. For every endpoint, ask three questions: who can call it, what may they do with it, and what does it actually return. Then walk every object reference and ask whether ownership is checked, because that is where the real-world breaches live.
Start with an inventory that is actually true
You cannot threat model endpoints you do not know exist, and almost every API has some. The published specification describes what the team believes it built; the router describes what it actually serves. Between them sit the endpoints added under deadline and never documented, the internal-only routes that were never meant to be externally reachable, the debug and health endpoints that leak more than intended, and two prior API versions still running because a mobile client from 2024 has not been retired.
| Source | What it tells you |
|---|---|
| OpenAPI spec / published docs | What the team intended to expose. Where you start, never where you stop. |
| The router or framework route table | What the service actually serves. The single most reliable source, and frequently the longest list. |
| Gateway / ingress configuration | What is reachable from outside, which is not the same as what exists. |
| Production access logs | What is genuinely being called, by whom, and how often. Reveals live use of endpoints everyone believed were dead. |
| Client code | Which fields callers actually send — including ones the spec never mentioned but the server happily accepts. |
Reconciling those five takes maybe an hour and it routinely produces findings before the threat modeling has formally started. "This endpoint is in the router, not in the spec, and got 40,000 calls last month" is a conversation worth having on its own.
Tier the endpoints, then model representatives
A mature API has hundreds of endpoints and you are not going to model each one. But you do not need to, because endpoints are not independent — most of them are instances of a small number of patterns, and the pattern is what carries the threat. Group them by exposure:
| Tier | Examples | What dominates the threat model |
|---|---|---|
| Unauthenticated | Login, registration, password reset, public search, webhooks | Abuse and enumeration. Anyone on the internet is a valid caller, so rate limiting and response uniformity are the controls. |
| Authenticated, own data | Profile, orders, documents, settings | Object-level authorization. The caller is legitimate; the question is whether this particular object is theirs. |
| Cross-tenant or cross-user | Team management, shared resources, org-wide reports | Tenant isolation, and whether it is enforced in the query or only in the interface. |
| Administrative | User management, billing overrides, feature flags, impersonation | Function-level authorization, and whether the route is protected by anything more than being undocumented. |
| Internal / service-to-service | Anything meant to be called only by your own services | Whether "internal" is a network fact or an authentication fact — usually the former, which is not a control. |
Take one representative endpoint per tier and model it properly. Then, for every other endpoint in that tier, the question is narrow and fast: does this one follow the pattern, or does it deviate? The deviations are the findings. This is the same move that makes a microservices model tractable — do the deep work once on the shared shape, then hunt for deltas — and it has the same benefit, which is that you end up with a statement about the whole API rather than deep coverage of the six endpoints you had time for.
Three questions per endpoint
Who can call it? Not "is it authenticated" but what specifically is being validated. If it is a bearer token, is the signature checked, is the algorithm pinned, is the audience checked, is expiry enforced, and can a token minted for a different service or a different environment be replayed here? If it is an API key, what is its scope, where does it live on the client, and how is it rotated? A token that is validated for shape but not for audience is a token that any system sharing the signing key can forge access with.
What may they do with it? This is where APIs actually fail, and it splits into two questions that are easy to conflate. Function-level: is this caller allowed to invoke this operation at all — can a normal user reach the admin route by knowing its path, or `DELETE` a resource whose UI only ever offers `GET`? Object-level: is this caller allowed to touch this specific object — and that one gets its own section below, because it is the single most consequential question on this list.
What does it actually return? Endpoints tend to serialise whole domain objects because that is what the ORM makes easy, and the interface only displays four of the fifteen fields, so nobody notices the other eleven going over the wire. Internal identifiers, other users' names, soft-deleted rows, password reset state, internal pricing, flags describing other tenants: all of it is in the response body, all of it invisible in the product, all of it trivially visible to anyone who opens developer tools. Ask what the response contains, not what the screen shows.
Object-level authorization: the one that keeps happening
Take GET /api/orders/1042. The endpoint requires a valid session, so it is authenticated. It requires the customer role, so it is authorized. And it returns order 1042 to anyone holding any valid customer session, because at no point does it check that order 1042 belongs to this customer. Change the number, get somebody else's order. The endpoint passed every check it had; it simply never had the check that mattered.
This is Broken Object Level Authorization — the flaw the OWASP API Security Top 10 has ranked first since the list was introduced, and its 2023 edition keeps it at number one. It stays there because it is not a bug in a library you can patch, it is an omission that has to be prevented individually at every endpoint that accepts an identifier, and one forgotten check is a full data exposure. It is also nearly invisible in code review, since the endpoint that has the check and the endpoint that lacks it look almost identical.
In a threat modeling session it is cheap to find, because you go looking for it systematically rather than hoping to notice it. Walk the endpoint list and, for each one that takes any object reference — a path parameter, a query filter, an ID in the body, a filename, a nested reference inside a JSON document — ask one question: where is the line of code that ties this object to this caller? If the room cannot point at it, that is a finding, and it does not matter how confident anyone is that "the service layer handles it." Pay particular attention to the second-order references: an endpoint that correctly checks ownership of the invoice and then returns a linked customer record it never checked at all is the standard shape of this bug in a codebase that mostly gets it right.
The STRIDE sweep, at endpoint altitude
| Category | What to ask of an endpoint |
|---|---|
| Spoofing | What proves the caller is who they claim? Is the token's signature, algorithm, audience, and expiry all validated? Can a token from another environment or tenant be replayed here? |
| Tampering | Which fields in the request body does the server accept and bind? Mass assignment — a client setting role, is_verified, account_id, or price because the model binds everything it receives — is the classic finding here. |
| Repudiation | Is every state-changing call logged with the acting identity, the object touched, and a request ID? Read endpoints that expose sensitive data deserve this too. |
| Information disclosure | What is in the response beyond what the client needs? Do error messages distinguish "not found" from "not yours"? Are identifiers sequential, and do they end up in URLs, and therefore in logs, proxies, and referrer headers? |
| Denial of service | Is there a maximum page size, or will ?limit=1000000 be honoured? Are filters and sorts bounded to indexed columns? Are rate limits per identity or per IP — and can bulk endpoints amplify one request into thousands of operations? |
| Elevation of privilege | Is the admin route protected by a check or by obscurity? Are all HTTP methods on a path authorized, or only the one the interface uses? Can a caller move between tenants by changing an identifier in the body? |
What the interface was hiding
A handful of API-specific threats come purely from the loss of the interface's implicit constraints, and they are worth asking about explicitly because no diagram element will prompt them:
- Order. The client calls the endpoints in the sequence the flow defines. An attacker calls step three without steps one and two — confirming an order that was never paid for, activating an account that never verified an email. Every multi-step flow needs its state checked server-side at each step, not just tracked client-side across them.
- Method. Authorization is often applied to a path rather than a path plus method, so an endpoint that shows a resource to any authenticated user also lets them delete it.
- Volume and timing. The interface makes one call per user action; a script makes ten thousand. Enumeration, credential stuffing, and scraping are all just the same endpoint used at machine speed.
- Fields. The form posts the fields it renders. A raw request posts whatever it likes, and the server binds whatever it accepts.
- Old versions.
/v2/got the new authorization logic;/v1/is still routed, still serving, and still enforcing the rules from two years ago. Deprecated is not the same as unreachable, and this belongs on the checklist for exactly the reason it keeps being missed — nobody is looking at v1 any more.
What to record
Record findings against the endpoint tier where they are patterns, and against the specific endpoint where they are exceptions. "No endpoint in the authenticated-own-data tier verifies object ownership in a shared helper" is one risk register entry with one systemic fix; nineteen separate entries for nineteen endpoints is the same finding recorded in a way that guarantees several get closed and the rest do not. Where the fix is genuinely systemic — a shared authorization middleware, a serialiser allowlist, a default page-size cap — say so in the entry, because that framing is what gets it prioritised over nineteen small tickets.
Then write down the assumptions, the same as any other model: which endpoints you believed were unreachable from the internet, which token properties you believed were validated, which fields you believed the serialiser excluded. Those are the claims the model rests on, and in an API they go stale unusually fast, because attack surface here grows one route at a time as a normal, healthy consequence of shipping features. A new endpoint is a new element in the model, and the useful trigger for revisiting is not a date on the calendar but a merge that adds a route.
Where API threat models go wrong
- Modelling the spec instead of the router. The spec is what you meant to expose; the router is what you exposed.
- Stopping at authentication. "It requires a valid token" answers who is calling, not what they are allowed to touch — and the second question is where the breaches are.
- Treating the interface as a control. Anything the client enforces is a suggestion. Only the server enforces.
- Checking the happy path only. The threats live in the calls the client would never make: wrong order, wrong method, wrong ID, extra fields.
- One entry per endpoint. Buries the systemic finding under dozens of instances of itself.
- Forgetting the old versions. Deprecated endpoints are still endpoints, and they are running the authorization logic you have since replaced.
For the endpoints that carry the most, it is worth going one step past the list. Take a goal an attacker would actually have — "read another tenant's records" — and build the attack tree beneath it: which endpoints could serve that goal, what each would require, and which of those requirements are genuinely independent. That is what tells you whether the API has real depth behind its authorization layer or a single check standing between a valid session and everybody's data.
Payment and open-banking APIs carry the strictest version of all of this, since PCI DSS treats the endpoints touching cardholder data as in-scope system components with their own evidence requirements. See threat modeling for fintech, or start from the pre-labelled DFD in the free threat modeling templates.