Your operationIds are your public method names, and unique is not enough
An operationId is not documentation metadata. It is the name your users type. Here is why uniqueness does not save you, and what actually collides.

Most specs treat operationId as a formality. It is a required-ish field, some tool once complained about it, somebody filled it in, and nobody looked again.
Then you generate an SDK, and that field is the name every one of your users types.
await client.actions.actionsActionsGet();That call site is not a generator bug. It is ActionsActionsGet, written into a spec by a person who assumed nobody would read it, surviving all the way to a consumer's editor.
The id is the name, and the namespace is the tag
Two fields decide what a caller writes. The tag decides the namespace. The operationId decides the method.
By default the generated client groups operations by tag and strips the namespace word from the method, because repeating it reads badly. So getBalance, tagged balance, becomes:
await client.balance.get();That is the whole mechanism. It is also why ActionsActionsGet under an actions tag produces the call above: there is no namespace word to strip cleanly out of a name that was never written for a call site.
Rename the operationId later and you have renamed a public method. Your users' code stops compiling. Nothing in the spec diff will look like an API change, because the wire operation is identical: same method, same path, same response. The break is entirely in the name.
Unique operationIds are not enough
Here is the part that surprises people, and the reason we changed how our own spec audit checks this.
The obvious rule is "no two operations share an operationId". Every linter has it. It is necessary and it is not sufficient, because the thing that has to be unique is not the id you wrote. It is the name a caller ends up typing, which is the namespace and the method together, after the namespace word has been stripped.
Consider two perfectly distinct ids:
/users/{id}:
get:
tags: [users]
operationId: getUser
/users/{id}/profile:
get:
tags: [users]
operationId: usersGetBoth are unique. Both are tagged users. Both reduce to client.users.get(). One of them has to be renamed by the generator, and whichever one loses is now a method your users cannot predict from reading your spec.
That is the failure our audit checks for now. It runs the generator's own canonical naming plan over the document and compares the public names, not the authored ids, which means it catches collisions that no OpenAPI linter will tell you about because at the spec level nothing is wrong.
What good ids look like
Name the action, not the route.
# The route is already in the path. Repeating it costs your users a word
# on every call and buys nothing.
operationId: listInvoices # not: getV1InvoicesList
operationId: createPaymentIntent # not: paymentsPaymentIntentsPost
operationId: cancelSubscription # not: subscriptionCancelPostThree habits cover most of it:
Lead with a verb that says what happens. list, create, cancel, retrieve. This is the word that survives into the method name after the namespace is stripped, so it is the word doing the work.
Do not encode the HTTP method. Post, Get and Put suffixes are a transport detail leaking into a name that is meant to hide the transport. A caller who wanted to think about verbs would have used fetch.
Do not repeat the tag. If the operation is tagged invoices, invoicesListInvoices becomes invoices.listInvoices() at best and gets mangled at worst. The namespace already said it.
Missing ids are worse than bad ones
An operation with no operationId at all does not get skipped. The generator synthesizes a name from the path, because it has to call the method something.
That name is now coupled to your URL structure. Move /v1/users to /v2/users, or rename a path segment for tidiness, and every generated method name shifts underneath your users with no deprecation and nothing in your changelog, because as far as your spec is concerned you changed a path and not an API.
An operationId is the thing that pins the public name to something you control. Write one for every operation, including the boring ones.
Changing an id after you have shipped
You will get one wrong. Everybody does.
The instinct is to fix the spec, and the instinct is wrong once an SDK is published, because the fix renames a method your users have already written. If the name is bad enough to be worth a major version, do it deliberately and say so in your changelog. If it is not, override the generated method name for that endpoint instead. The public name stays stable while the spec underneath gets tidied, which is the outcome you actually wanted.
That override is also the escape hatch for a spec you do not own. Plenty of teams generate clients from a document produced by a framework annotation pass, where the ids are whatever the framework felt like emitting and editing them upstream is somebody else's sprint.
Check it before your users do
You can read your own spec for this, and you will miss the collisions, because the collision is not visible in the document. It only appears after tags, method naming and the generator's naming plan have been applied.
Our spec audit runs that plan and scores what comes out, including the two rules in this post: whether every operation declares an id, and whether the public names those ids produce actually stay distinct. It publishes all fourteen rules it checks, with the weight each one carries, so you can fix the expensive ones first.
If you want to see what the names become in a real client, the OpenAPI to TypeScript SDK and OpenAPI to Python SDK pages show the call sites the same spec produces in each language.
Your operationIds are already a public API. They may as well be a good one.



