At platform scale, API documentation is a distributed systems problem with ownership, versioning, release, security, and composition boundaries.
I recently looked closely at this problem in the context of a fintech payment gateway. The platform had a partner-facing API, several internal services, shared error contracts, generated Java interfaces, and a separate developer portal. The interesting work was not writing YAML. It was deciding where the YAML belonged, who could publish it, how a portal should combine it with human-written guidance, and what evidence was required before an integrator could trust it.
That architecture applies far beyond payments. Any platform with multiple services and external consumers eventually has to answer the same questions.
Start With the Consumer, Not the Repository
A payment platform can have several kinds of API consumers:
Merchants integrating payment acceptance directly
Independent software vendors embedding payments into vertical products
Payment facilitators onboarding and managing sub-merchants
E-commerce platforms creating charges, refunds, and recurring payments
Partner operations teams investigating settlements, disputes, and webhooks
Internal applications using administrative APIs that must never become public contracts
These consumers do not care how many microservices exist behind the API gateway. They care about completing a workflow.
A merchant wants to know how to accept a payment. An ISV wants to know how OAuth scopes, merchant onboarding, idempotency, webhooks, refunds, and certification fit together. Neither wants to reverse-engineer which internal service owns each operation.
That leads to the first architectural principle:
The developer portal should reflect the external product boundary, not blindly mirror the internal deployment topology.
You may have separate services for identity, merchant onboarding, payment authorization, tokenization, settlement, disputes, and notifications. That does not automatically mean the portal should expose seven unrelated API products. If clients enter through one stable gateway and experience one cohesive platform, the documentation should preserve that cohesion.
Internal boundaries still matter for ownership. They just should not leak into the consumer experience without a reason.
Why the Developer Portal Deserves Its Own Project
The first instinct is often to put documentation inside the main application repository and serve it from the service. That works when one service has one audience and one release lifecycle.
It breaks down as the platform grows.
A developer portal contains at least two different kinds of information:
Machine-readable contracts, such as OpenAPI documents and webhook schemas
Human-readable journeys, such as getting started, authentication, first payment, testing, certification, and go-live guidance
Those assets have different owners and different reasons to change.
The service team owns whether POST /payments accepts an idempotency key and what response it returns. The developer-experience team owns whether an integrator can discover that requirement before sending a duplicate charge. The security team owns the authentication model. The operations team owns the go-live and support procedures.
Putting the portal in a separate project creates a clean ownership boundary:
Service repositories
own executable API contracts
own endpoint implementation
own contract tests
publish versioned API artifacts
Developer portal repository
owns navigation and information architecture
owns integration guides and tutorials
owns branding and access control
assembles versioned API references
publishes the documentation site
The separation also gives the portal its own release cadence. A typo in a certification guide should not require redeploying the payment service. A new service version should not require a documentation writer to copy YAML manually. A portal preview should be buildable without starting the entire payment platform.
Separate does not mean disconnected. It means the connection must be explicit and automated.
The Source of Truth Must Stay With the Service
The portal should display the API contract, but it should not author the service contract.
If the portal repository becomes the source of truth for endpoint definitions, drift is almost guaranteed. One team changes the controller, another updates the portal days later, and integrators get to discover which version is real.
I prefer this ownership rule:
The repository capable of breaking the API is the repository responsible for defining and validating the API.
For an external payment API, that means the service repository owns:
Paths and HTTP methods
Request and response schemas
Authentication requirements and scopes
Idempotency headers
Error responses
Webhook payloads
Examples safe for external consumption
The API version and changelog
The portal receives an immutable, validated artifact from that repository.
This is particularly important in fintech. A generated example must not contain a real card number. An error schema must not expose processor diagnostics or internal identifiers. An internal administrative endpoint must not appear because a runtime documentation scanner found every controller in the application.
The public contract is a curated product surface, not a dump of implementation annotations.
Contract-First in a Payment Gateway
For partner-facing APIs, I prefer contract-first development.
The service team writes the OpenAPI source as small, reviewable fragments:
Path files describe operations. Component files describe reusable data structures and security schemes. A bundling step resolves those references into one self-contained gateway.yaml.
That bundle becomes the critical artifact. The same bytes should drive:
OpenAPI linting
Server interface and DTO generation
Contract-aware tests
Client SDK generation, if used
Developer-portal publication
This creates a strong direction of travel:
Authored contract
↓
Bundled contract
├── generated server interfaces
├── generated models
├── tests and compatibility checks
└── published API reference
The controllers implement generated interfaces. Changing an operation identifier, required parameter, or response type changes the Java contract. Compilation then becomes a useful guardrail.
Path-only changes may still compile because the Java method signature can remain the same. That is why HTTP tests must assert the route and wire format. Compilation and endpoint tests protect different parts of the contract.
Shared Contracts Across Services
Microservices create another problem: repeated definitions.
Every public service may need the same:
Problem-detail schema
Unauthorized and forbidden responses
Correlation identifier header
Pagination model
Idempotency error
Common authentication scheme
Copying these YAML files into every repository looks harmless. It is not. The copies drift, and a documentation compositor may be unable to merge components with the same name but different content.
A better pattern is a small shared contract artifact:
Publish that artifact to the organisation's package registry. Java teams can package it as a Maven artifact; other ecosystems can use an npm package, OCI artifact, or versioned archive. Each service pins an explicit version, unpacks it during the local contract build, and references those files while bundling.
private artifact registry
↓ download pinned version
service/openapi/api/.shared/
↓ bundle with service-owned paths and schemas
service/openapi/gateway.yaml
The shared package should contain platform-wide semantics, not business-domain models. A common error envelope belongs there. A payment authorization request does not. Centralising every schema creates a distributed monolith where no service can evolve independently.
Share vocabulary and protocol conventions. Keep domain ownership local.
Managing Contracts Across Multiple Microservices
There are two valid portal models, and choosing between them is an architecture decision.
Independent API products
Use independent API references when services expose distinct domains, credentials, release cycles, or client audiences.
Each service versions independently. The portal provides a consistent shell and cross-links the workflows.
One public platform API
Use a composed API when clients see one hostname, one authentication model, one version policy, and one coherent product contract even though several services implement it internally.
In that model, a release pipeline can join service-owned fragments into one public bundle. The composition stage must detect conflicting paths, duplicate operation identifiers, incompatible shared schemas, and inconsistent security definitions.
Do not join specifications simply because a tool can do it. Join them when the consumer experiences one API.
This distinction matters in payment platforms. Internal microservices often change without creating a new public API version. The public contract should move when the partner experience changes, not every time an internal service is deployed.
The Pull-Request Pipeline Is a Contract Gate
The service repository's pull-request workflow must treat an OpenAPI change as a build-requiring change.
Linting alone proves that the YAML is valid. It does not prove that generated interfaces compile, controllers still match, routes work, or JSON has the promised shape.
A practical PR pipeline looks like this:
detect contract change
↓
restore shared contract artifact
↓
bundle OpenAPI
↓
fail if committed bundle is stale
↓
lint bundled specification
↓
run breaking-change analysis
↓
generate server interfaces and models
↓
compile application
↓
run endpoint and security tests
↓
publish preview and test reports
The stale-bundle check is easy to miss. If developers commit both authored fragments and the generated bundle, CI should rebuild the bundle and fail when git diff is not empty. Otherwise, the source fragments can say one thing while downstream generation and publication use an old bundle.
Change detection should express the real dependency:
build_required=false
if [[ "$application_changed" == true || "$contract_changed" == true ]]; then
build_required=true
fi
Then Java setup, private-registry authentication, code generation, compilation, and tests use build_required.
I have seen pipelines classify YAML as documentation and skip the application build. That classification is wrong in a contract-first system. The YAML generates application code and routing metadata. It is executable design.
For payment APIs, the tests should cover more than a successful 200 response:
Unauthenticated calls return the documented format
Valid tokens without the required authority receive 403
Idempotency behaviour matches the contract
Card data remains masked in responses and logs
Validation failures use the shared problem schema
Undeclared internal fields never appear on the wire
Webhook signatures and retry semantics match the published guide
This is where a contract becomes operationally meaningful.
Deployment and Documentation Publication Must Be Ordered
The next boundary is between merging a contract and publishing it.
A common workflow starts two independent jobs from the same push:
merge to main
├── publish OpenAPI to developer portal
└── build, test, and deploy service
That is a race. The documentation job can finish while the application later fails compilation, tests, image creation, deployment, or health checks.
The safer sequence is explicit:
PR contract gate passes
↓
merge
↓
build immutable service artifact
↓
deploy exact commit to the target environment
↓
wait for deployment health
↓
publish the exact commit's OpenAPI artifact
In a job-based CI system, the publication job should depend on the deployment job:
The source-ref is not cosmetic. It prevents the publisher from checking out a newer commit that reached the default branch while the deployment was running. Documentation must come from the same revision that passed validation and reached the environment.
For mature release processes, publication and promotion can be separate states. A contract can appear as an upcoming version in a restricted preview after development deployment, then become the default reference only after the corresponding production release. The portal should model that state explicitly rather than inferring it from whichever workflow ran last.
What the Publishing Workflow Actually Does
The service repository should push a narrow, immutable release payload into the portal repository.
A publisher typically performs these steps:
Check out the validated service commit.
Lint the bundled contract one final time.
Read the API version from info.version.
Check out the portal using a least-privilege deploy identity.
Refuse to overwrite an existing version with different content.
Create a version directory.
Copy the bundle and changelog.
Write release metadata.
Update version manifests according to release state.
Open a bot pull request or commit through a protected automation path.
The immutability check matters:
If @1-4-0 exists and content is identical: no-op
If @1-4-0 exists and content differs: fail and require a version bump
If @1-4-0 does not exist: publish it
Once integrators have consumed a version, silently rewriting it destroys trust. A semver folder should behave like an immutable release artifact.
Direct bot commits are simple, but a bot pull request is often safer for a central portal. It allows portal CI to validate navigation, cross-links, version manifests, and the full site build before merge. Auto-merge can keep the flow fast after all checks pass.
How the Developer Portal Repository Should Be Structured
A useful portal structure keeps authored content and generated API releases visibly separate:
The ownership rule should be obvious from the tree:
Humans edit guides, navigation, branding, and conceptual documentation.
Service automation writes versioned API directories and release metadata.
Nobody hand-edits a published OpenAPI snapshot in the portal.
Versioned API references and unversioned guides can coexist. The API reference preserves historical contracts. The guides usually explain the current generally available workflow and call out upcoming changes through banners or scoped preview environments.
That choice avoids duplicating the entire documentation site for every patch release. Full-site versioning is justified only when the human workflow changes so dramatically that one set of guides cannot serve multiple supported versions.
Portal CI Has Its Own Responsibilities
The service pipeline proves that a contract matches an implementation. Portal CI proves that multiple contracts and human-authored content form a coherent site.
The portal pipeline should validate:
Every OpenAPI snapshot parses and renders
Version manifests refer to existing directories
Default pointers refer only to approved releases
Navigation contains no stale hard-coded version links
Markdown links and included partials resolve
Shared component names do not conflict during composition
Generated directories were changed only by the publishing identity
Examples contain no secrets, live credentials, or regulated data
The complete portal builds successfully
For a payment platform, I would add automated checks for unsafe examples. Test card data must come from documented sandbox ranges. PAN-like values should be masked unless explicitly permitted test values are used. Private processor response fields and internal support identifiers should never escape into public schemas.
The portal should also create a preview for every change. Documentation has UI behaviour: navigation, search, version pickers, code samples, and responsive layouts can break even when every Markdown file parses.
Versioning Across Services Is a Product Decision
Application versions, container image versions, and API versions are not the same thing.
A payment service might deploy twenty times without changing its public contract. Conversely, a contract can change before implementation reaches production so partners can review an upcoming version.
Keep the identities separate:
Application build: internal release cadence
Image digest: immutable deployable artifact
API version: external compatibility promise
Portal version: rendered documentation for that promise
In a multi-service platform, avoid one global API version unless the services genuinely release as one public contract. Independent public products should have independent versions. A global version number couples unrelated teams and creates artificial release trains.
The portal can still provide one integrated experience. It simply needs a version manifest per API product and cross-service guides that describe compatible combinations where necessary.
Ownership and Governance
This architecture works only when ownership is explicit.
I use a model like this:
This architecture works only when ownership is explicit.
I use a model like this:
Asset
Primary owner
Required reviewers
Service paths and domain schemas
Service team
API/domain owners
Shared errors, headers, and auth vocabulary
Platform team
Security and service representatives
Authentication and authorisation guidance
Identity/security team
Developer experience
Integration journeys and tutorials
Developer-experience team
Relevant service teams
Version manifests and published snapshots
Automation
Portal CI and release policy
Portal configuration and navigation
Developer-experience/platform team
Product and security where relevant
CODEOWNERS can encode part of this, but review rules are not enough. Teams need a compatibility policy:
What counts as breaking?
Which semantic-version change is required?
How long are old versions supported?
Can prerelease contracts change in place?
Who approves making a version the default?
How are deprecated fields communicated?
What happens when a deployment rolls back?
Those are API product decisions. YAML cannot answer them.
CODEOWNERS can encode part of this, but review rules are not enough. Teams need a compatibility policy:
What counts as breaking?
Which semantic-version change is required?
How long are old versions supported?
Can prerelease contracts change in place?
Who approves making a version the default?
How are deprecated fields communicated?
What happens when a deployment rolls back?
Those are API product decisions. YAML cannot answer them.
Failure Patterns I Watch For
Several patterns repeatedly create false confidence.
Runtime-generated documentation as the only source
Scanning controllers is convenient, but it tends to expose implementation rather than a curated external product. It also makes documentation availability depend on a running service.
A portal-owned copy of every specification
Manual copying breaks ownership. The service and portal eventually disagree, and nobody knows which one represents the promise.
Lint-only checks for contract changes
Valid OpenAPI can still generate uncompilable interfaces, break routes, or contradict wire behaviour.
Publishing and deployment in parallel
The portal can advertise a version that failed to reach the environment.
Shared schemas copied into every service
Small differences accumulate until composition fails or clients receive inconsistent error formats.
One global version for unrelated APIs
Every service becomes part of one release train even when consumers do not experience them as one product.
Hard-coded links to a particular version
The version manifest advances, but navigation still sends users to an obsolete reference. Stable overview routes or generated navigation prevent this.
Letting the portal reveal internal topology
Consumers see service boundaries, internal names, and operational APIs that have no place in the public product.
The important property is direction. Contracts move from owning services into the portal. Human guidance and portal configuration stay in the portal. Shared protocol definitions come from a deliberately small common package. Deployment success gates publication.
Summary
Keep the developer portal in a separate project
Why*:* Documentation has its own owners, content types, release cadence, access model, and build lifecycle.
Keep executable contracts with the owning services
Why*:* The team capable of changing behaviour remains responsible for validating the promise.
Design the portal around consumer journeys
Why*:* External developers should not have to understand internal microservice topology.
Use contract-first generation for partner APIs
Why*:* The published contract, generated interfaces, models, and tests share one source.
Package only truly shared API components
Why*:* Services stay consistent without centralising domain ownership.
Treat contract changes as application changes in CI
Why*:* Linting, generation, compilation, and endpoint tests catch different failures.
Publish only after the exact commit deploys successfully
Why*:* The portal describes software that is demonstrably available.
Store immutable, versioned API snapshots
Why*:* Integrators can rely on historical contracts and audit what changed.
Give portal CI independent responsibilities
Why*:* Cross-links, navigation, composition, security, and rendering require system-level validation.
Final Thoughts
The easy part of API documentation is producing pages. AI will keep making that easier. It can write schemas, generate examples, scaffold workflows, and create a polished portal shell before lunch.
The difficult part is preserving truth across boundaries. Which repository owns the promise? Which build proves the implementation can keep it? Which deployment makes it available? Which portal version should an integrator trust? Which internal details must never cross the public boundary?
Those decisions require architecture, not autocomplete.
When the system is designed well, the developer portal stops being a website that contains API files. It becomes the verified, versioned projection of what the platform is prepared to support.
If your platform has more than one service and more than one API consumer, share this with the person designing how those contracts reach developers.