Where an Internal API Should Stop and a Public One Starts
Somewhere in a codebase, there is a function that returns a customer's billing address, and three different teams call it. The mobile app calls it to prefill a checkout form. The fraud service calls it to compare against a cardholder's home country. A reporting job calls it once a night to build a regional sales breakdown. The function has grown four optional parameters over two years, each one added for exactly one caller, each one defaulted in a way that only that caller understands. Nobody wrote a specification. Nobody published a version. The function is, in the most literal sense, an internal API: a boundary that exists only because the code on both sides was written by people who could walk over and ask.
The interesting question is not whether that function is messy. Internal APIs are allowed to be messy, the same way a family kitchen is allowed to have a drawer full of mismatched utensils. The question is when a company should stop pretending the messy thing is private, and commit to a boundary that strangers can rely on.
The line between internal and public is usually crossed by accident, in increments, and the cost of pretending it has not happened is paid later by people who were never in the room for the decision.
What "internal" actually promises
An internal API makes a promise to colleagues. The promise is loose: call this, and it will do roughly what the name says, as long as the team that owns it stays employed and nobody reorganizes the department. Change is expected. Breaking changes are annoying but survivable, because the broken caller sits three desks away and can be told in person. The docstring is often a single line. The error handling is whatever the first author happened to write at 11 p.m.
A public API makes a different kind of promise. It promises to strangers, at a distance, over time. The stranger will read a spec, write code against it, and ship a product. If the boundary shifts, the stranger's product breaks, and the stranger may never know why. That promise costs real money to keep. It requires versioning, deprecation windows, documentation that survives staff turnover, and a support channel that does not rely on the original author's cell number.
Everything else about API design follows from that difference in promise. The question is not about technology. It is about who is allowed to be surprised.
The moment a second company depends on it
A common story: a partner integration gets built quickly. The partner's engineering team is small, the timeline is short, and the fastest path runs through an endpoint that was never meant for outside consumption. Someone adds an API key. A page in the internal wiki gets a new paragraph. The integration works. Everyone is pleased.
Six months later, the endpoint's owning team wants to refactor it. They cannot. The partner is now a revenue line. The refactor becomes a negotiation, then a project, then a permanent tax. The API was public the moment the partner's code shipped against it, regardless of what the wiki said. The label "internal" had become a fiction that everyone agreed to maintain because the alternative was admitting the boundary had already been crossed.
The honest test is simple and uncomfortable: if this endpoint broke tomorrow, who would be angry, and would that anger arrive as a support ticket from outside the company? If the answer is yes, the API is already public. The only remaining choice is whether to treat it that way.
Contract tests as the real dividing line
A useful diagnostic lives in the test suite. Internal APIs are typically tested through their callers. The tests exercise the caller's behavior and happen to touch the API along the way. If the API changes, the caller's tests break, someone fixes them, and life continues.
Public APIs need tests that stand on their own, written against a documented contract, passing or failing without reference to any particular consumer. A contract test suite is the clearest signal that a team has decided to make a promise. It is also the most annoying thing to build, which is why it is the dividing line that gets skipped.
A team that has written contract tests, published a version, and set a deprecation policy has crossed into public territory whether or not the endpoint is behind a firewall. A team that has not done any of those things is still internal, no matter how many external partners have quietly been given credentials.
Versioning is a decision about strangers
Internal APIs usually do not version. The main branch is the version, and callers adjust. This works because the callers are known and can be updated in a coordinated release. The coordination cost is paid in meetings and pull requests, and it is real, but it is bounded by the size of the company.
Public APIs version because coordination is impossible. A version number is an acknowledgment that the author cannot see all the callers and cannot update them. It is a way of letting old strangers keep working while new strangers get the better thing. A team that adds versioning to an endpoint has, in effect, admitted it no longer knows who is on the other side.
This is why half-versioned APIs are such a bad smell. A URL with a v1 in it and no v2 on the roadmap is a promise nobody intends to keep. A version number that changes in breaking ways without a deprecation window is worse: it advertises a contract and then breaks it, which trains consumers to distrust the boundary itself.
Authentication is not the boundary
The presence of an API key is often mistaken for the line. It is not. Internal services use keys and tokens too, sometimes with elaborate rotation schedules, because the security team asked for them. A key proves that a caller is authorized. It says nothing about whether the caller is a colleague or a stranger, and nothing about whether the boundary is expected to hold for years.
A better signal is the identity of the caller. Internal callers are service accounts owned by teams inside the company. Public callers are accounts owned by organizations the company does not control and cannot page. Once the second kind of account exists, the boundary has moved, whether or not the authentication mechanism noticed.
The documentation gap as an early warning
Internal APIs are documented in the code, in a README two directories up, or in a Confluence page last edited during the previous reorg. Public APIs are documented in a place a stranger can find, with examples that assume no prior knowledge of the company's internal vocabulary.
The size of the gap between those two documentation styles is a decent proxy for how far across the line a given API has drifted. If the only way to learn the endpoint is to ask someone on Slack, the API is internal and should stay that way, or should be rewritten before it is exposed. If a stranger can succeed with nothing but the published docs, the boundary is already public in spirit.
This is also where the cheapest intervention lives. A team that suspects an internal API is about to become public can spend a week writing the stranger-grade documentation before the first external caller arrives. Doing it after is dramatically more expensive, because the documentation then has to describe behavior that existing consumers already depend on, including the accidental parts.
When the internal API should be rebuilt, not relabeled
There is a temptation, once an internal API has acquired external callers, to declare it public by fiat. Add a version prefix, publish the existing docs, and move on. This usually fails, because the internal API was shaped by internal needs: it exposes database identifiers, it accepts parameters that only make sense given the company's data model, it returns error messages written for developers inside the building.
A genuinely public API is a different artifact. It hides internal identifiers behind stable ones. It uses vocabulary the outside world already understands. It fails in ways that help a stranger debug without access to the company's logs. Relabeling an internal API as public is like putting a hotel sign on a family home: the rooms are the wrong shape, and the guests will notice.
The right move, when the boundary is crossed, is usually to build a thin public surface that wraps the internal one. The wrapper translates identifiers, normalizes errors, and absorbs internal churn. The internal API stays free to change. The public API stays stable. The two promises remain separate, which is the entire point.
The cost of leaving the label wrong
An API mislabeled as internal when it is effectively public produces a specific kind of slow damage. External consumers build against undocumented behavior. The owning team refactors freely, believing no one outside depends on the details. Consumers break. Trust erodes. The owning team, surprised, adds defensive code and stops refactoring altogether, and the endpoint ossifies into something nobody can improve and nobody can retire.
The opposite error, labeling something public before it is ready, is louder and shorter. The endpoint gets exposed, the docs are thin, the first external integration hits an edge case, and the company spends a quarter patching a surface it should have designed first. Painful, but visible, and usually corrected.
The quiet error is the dangerous one. It accumulates for years, and it is discovered only when someone tries to change something and finds the change is no longer theirs to make.
A short list of questions worth asking before the next integration
Four questions tend to settle the matter faster than any architecture review. First, if this endpoint disappeared tomorrow, would anyone outside the company notice? Second, can a stranger succeed with only the published documentation? Third, is there a contract test suite that does not depend on any particular caller? Fourth, has anyone committed to a deprecation window in writing?
Three or four yes answers mean the API is public and should be treated as such. Zero or one means it is internal and should stay behind the wall until someone is prepared to make the promise. The middle answers are the interesting ones, because they describe the state most companies actually live in: an API that has drifted halfway across the line while everyone involved pretends it has not.
The halfway state is survivable for a while. It becomes expensive the moment a team tries to change the endpoint and discovers the change is now a negotiation with people they have never met. The boundary is not a label applied after the fact. It is a promise, and the promise begins the moment the first stranger's code depends on the answer.

